ADLAB PowerShell source file: lib-buildup.ps1

(C) Ondrej Sevecek, 2019 - www.sevecek.com, ondrej@sevecek.com



# ============================================================
# Note: Make sure that the lib-common has been loaded       ||
#                                                           ||

if (($global:adlabVersion -lt 1) -or (-not $global:libCommonScriptInitialized)) {

  $msgLibCommonNotInitialized = 'ADLAB: lib-common not loaded or initialized. Exiting'
  Write-Host $msgLibCommonNotInitialized -ForegroundColor Red
  throw $msgLibCommonNotInitialized
  exit 1
}

$adlabVersionThisLib = 985
$adlabReleaseDateThisLib = [DateTime]::Parse('2019-09-20')
DBG ('Library loaded: {0} | v{1} | {2:yyyy-MM-dd}' -f (Split-Path -leaf $MyInvocation.MyCommand.Definition), $adlabVersionThisLib, $adlabReleaseDateThisLib)
 
#                                                           ||
# Note: Make sure that the lib-common has been loaded       ||
# ============================================================



$global:VM_READY = 'VM-Ready' 

$global:VM_STATUS_OK = 0x00000000 # not finished yet, but intermediate restart required
$global:VM_RECYCLE_REQUIRED = 0x00000001 # not finished yet, but intermediate restart required
$global:VM_FINISHED_RESTART_REQUIRED = 0x00000002 # finished, restart required
$global:VM_FINISHED_NO_RESTART = 0x00000003 # finished, restart is not required
$global:VM_STATUS_FAIL = 0x00000004 # not finished yet, but intermediate restart required

$global:buildupRootFolder = Split-Path (Split-Path $MyInvocation.MyCommand.Definition -Parent) -Leaf
$global:buildupRegKey = "HKLM:\SOFTWARE\$(Split-Path -Leaf $global:libCommonParentDir)"

$global:emptyFiller = '!sevecek-buildup-empty-filler.dat'
$global:diskNoInitialization = '!sevecek-disk-noInitialization.txt'
$global:installSourceMedia = '!sevecek-installation-source-media.txt'
$global:installSourceMediaWildcard = '!sevecek-installation-source-media-*.txt'
$global:builderVhdxMarker = '!sevecek-builder-vhdx.txt'
$global:toolsISO = '!sevecek-tools-iso.txt'
$global:autounattendVFD = '!sevecek-buildup-autounattend.vfd'
$global:autounattendISO = '!sevecek-buildup-autounattend.iso'

$global:sevecekBuildupRoot = '!Sevecek-Buildup'
$global:precompiledConfigFilesFolder = 'Precompfig'
$global:buildupFolderPathOnSystemDisk = "TEMP\$global:sevecekBuildupRoot"
$global:buildupFolderPathOnSystemDiskAbsolute = Join-Path $env:SystemDrive $global:buildupFolderPathOnSystemDisk
$global:buildupFolderNameOnVhdx = 'BUILDER'
$global:buildupMainBAT = "%SYSTEMDRIVE%\$buildupFolderPathOnSystemDisk\buildup-main.bat"
$global:sevecekBuildupWritableFolders = @('BitColdKit', 'VolatileForensics', 'IIS\SevecekSimpleHttpServer')


$global:powerShellLinkLocations = @{

   (           '%programdata%\Microsoft\Windows\Start Menu\Programs\Accessories\Windows PowerShell')         = @( 'perSys', '6.0clt', '6.0srv', '6.1clt', '6.1srv'                                                               )
   (           '%programdata%\Microsoft\Windows\Start Menu\Programs\System Tools')                           = @( 'perSys',                                         '6.2clt', '6.2srv', '6.3clt', '6.3srv'                       )
   (           '%programdata%\Microsoft\Windows\Start Menu\Programs\Administrative Tools')                   = @( 'perSys',                                                   '6.2srv',           '6.3srv'                       )
   ('%sevecek_defaultprofile%\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Windows PowerShell')     = @( 'perSys',                                                                                 '10.0clt', '10.0srv' )

   # Note: the 'Windows System' is just an Explorer alias defined in desktop.ini
   (           '%programdata%\Microsoft\Windows\Start Menu\Programs\Windows System')                         = @( 'perSys'                                                                                                       )

   # Note: the 'User Pinned\Taskbar' item gets copied from the %programdata% source
   (   '%sevecek_userprofile%\AppData\Roaming\Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar') = @( 'perUsr',                               '6.1srv',           '6.2srv',           '6.3srv'                       )
   (   '%sevecek_userprofile%\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Windows PowerShell')     = @( 'perUsr',                                                                                 '10.0clt', '10.0srv' )

 }


function global:Load-VMConfig()
{
  DBG ('Loading vmConfig for: {0}' -f $vmName)
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $vmName }
 
  $global:vmConfig = $null
 
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $xmlConfig }
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $xmlConfig.VMs }
  $loadedNode = $xmlConfig.VMs.SelectSingleNode('./MACHINE/vm[@name="{0}"]/..' -f $vmName)
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $loadedNode.hostName }
  
  if (Is-ValidString $loadedNode.hostName) {

    $global:vmConfig = $loadedNode
    DBG ('Loaded vmConfig for VM: vm = {0} | hostName = {1}' -f $vmName, $vmConfig.hostName)
  }
}


function global:Load-PhaseConfig ()
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  $global:phaseCfgPath = Join-Path $global:rootDir '!phaseId.xml'
  DBG ('Loading phase config XML: {0}' -f $global:phaseCfgPath)
  DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path $global:phaseCfgPath) }
  DBGSTART
  $global:phaseCfg = Load-XmlSafe $global:phaseCfgPath #[XML] (Get-Content $global:phaseCfgPath)
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
}


function global:Validate-XmlConfig ()
{
  $ips = $xmlConfig.VMs.SelectNodes('./MACHINE/net') | Select -Expand ip | % { foreach ($one in (Split-MultiValue $_)) { if ($one -ne $global:emptyValueMarker) { $one } } } | sort

  if ((Get-CountSafe $ips) -gt 0) {

    $duplicateIPs = Find-DuplicatesInSortedList $ips
    DBGIF ('There are some DUPLICATE IPs in the config: {0}' -f ($duplicateIPs -join ',')) { ((Get-CountSafe $duplicateIPs) -gt 0) }
  }


  $subnetMap = Get-NetworkMap | Select -Unique hvNetwork, nicName, subnetIP, mask | Sort nicName
  if ((Get-CountSafe $subnetMap) -gt 0) {

    if ((Get-CountSafe ($subnetMap | Select -Unique nicName)) -lt (Get-CountSafe $subnetMap)) {

      for ($i = 0; $i -lt ((Get-CountSafe $subnetMap) - 1); $i ++) {

        if (($subnetMap[$i].nicName -eq $subnetMap[$i+1].nicName) -and ($subnetMap[$i].hvNetwork -ne $subnetMap[$i+1].hvNetwork)) {

          DBGIF ('You have two or more same internal NIC names assigned to different Hyper-V switches: nic = {0} | hvSwitch1 = {1} | hvSwitch2 = {2}' -f $subnetMap[$i].nicName, $subnetMap[$i].hvNetwork, $subnetMap[$i+1].hvNetwork) { $true }
        }
      }
    }
  }


  $nlbDefs = $networkMap | ? { Is-ValidString $_.nlbInstance } | select -Unique nlbInstance, ipAddress
  if ((Get-CountSafe $nlbDefs) -gt 0) {

    $nlbDefs = $nlbDefs | sort nlbInstance
    [string] $prevNlbInstance = $null
    foreach ($oneNlbDef in $nlbDefs) {

      DBGIF ('You have different IP addresses for the same NLB instance: {0}' -f $prevNlbInstance) ($oneNlbDef.nlbInstance -eq $prevNlbInstance)
      $prevNlbInstance = $oneNlbDef.nlbInstance
    }

    $nlbDefs = $nlbDefs | sort ipAddress
    [string] $prevNlbIP = $null
    foreach ($oneNlbDef in $nlbDefs) {

      DBGIF ('You have same IP addresses for different NLB instances: {0}' -f $prevNlbIP) ($oneNlbDef.ipAddress -eq $prevNlbIP)
      $prevNlbIP = $oneNlbDef.ipAddress
    }
  }



  [string[]] $hostNames = $xmlConfig.VMs.SelectNodes('./MACHINE') | Select -Expand hostName | sort

  if ((Get-CountSafe $hostNames) -gt 0) {

    $duplicateHostNames = Find-DuplicatesInSortedList $hostNames
    DBGIF ('There are some DUPLICATE HOSTNAMEs in the config: {0}' -f ($duplicateHostNames -join ',')) { ((Get-CountSafe $duplicateHostNames) -gt 0) }
  }


  [string[]] $vmNames = $xmlConfig.VMs.SelectNodes('./MACHINE/vm') | Select -Expand name | sort

  if ((Get-CountSafe $vmNames) -gt 0) {

    $duplicateVmNames = Find-DuplicatesInSortedList $vmNames
    DBGIF ('There are some DUPLICATE VM NAMEs in the config: {0}' -f ($duplicateVmNames -join ',')) { ((Get-CountSafe $duplicateVmNames) -gt 0) }
  }


  $invalidVMs = $null
  $invalidVMs = $xmlConfig.SelectNodes('./VMs/MACHINE[string-length(@hostName)>15]')
  DBGIF ('Too long hostName not tested/supported: {0}' -f (($invalidVMs | select -Expand hostName) -join ',')) { (Get-CountSafe $invalidVMs) -gt 0 }


  $allVMs = $xmlConfig.VMs.SelectNodes('./MACHINE')
  DBG ('Verify config for each machine defined: #{0}' -f (Get-CountSafe $allVMs))
  if ((Get-CountSafe $allVMs) -gt 0) {

    foreach ($oneVM in $allVMs) {

      DBG ('Checking XML configuration for machine: {0}' -f $oneVM.hostName)

      $localFSsFound = $oneVM.SelectNodes('./*/fs[@instance="local" and @appTag="jobs"]')
      DBGIF ('There are some duplicate local FS job folders in the config: {0}' -f $oneVM.hostName) { (Get-CountSafe $localFSsFound) -gt 1 } 
    }
  }

  
  [Collections.ArrayList] $allDNSnames = $hostNames
  if ((Get-CountSafe $allVMs) -gt 0) {

    foreach ($oneVM in $allVMs) {

      $vMDnsNames = Get-ServiceOrMachineDnsFQDNs $oneVM
    
      foreach ($oneVMDnsName in $vMDnsNames) {

        DBGIF ('Duplicate DNS name found: vm = {0} | dns = {1}' -f $oneVM.hostName, $oneVMDnsName) { Contains-Safe $allDNSnames $oneVMDnsName }
        [void] $allDNSnames.Add($oneVMDnsName)
      }
    }
  }


  DBG ('Will validate the forests configuration: {0}' -f (-not (Parse-BoolSafe $xmlConfig.vmBuilder.validation.noForest)))

  if (-not (Parse-BoolSafe $xmlConfig.vmBuilder.validation.noForest)) {
    
    $forests = Get-Forests
  
    foreach ($oneForest in $forests) {
  
      $forestMachines = Get-ForestMachines $oneForest

      $rodcs = $forestMachines | ? { $_.isRODC }
      DBG ('RODC found in config: {0}' -f (Get-CountSafe $rodcs))
      DBGIF ('Multiple RODCs in a single site') { (Get-CountSafe $rodcs) -ne (Get-CountSafe ($rodcs | select -Unique adSite)) }
    }
  }
}


function global:Get-SysprepUnattendedXmlPKElement ([string] $pk)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
 
  [string] $sysprepPK = ''

  if (Is-ValidString $pk) {

    $sysprepPK = @"
                    
                        OnError
                        $pk
                    
"@

  } else {

    $sysprepPK = ''
  }

  return $sysprepPK  
}


function global:Install-BuildupFolder ([string] $buildupSrc, [string] $path, [string] $softwareKey, [string] $onlineRootDrive, [string] $onlinePath, [bool] $installingFromISO, [string] $defaultAdminPwd)
# Note that the function must support offline VHD image injection
# as well as an online deployment into a current operating system,
# thus the parameters have the following meanings:
#
#   buildupSrc = absolute source path to copy from
#   path = absolute target path to copy to, absolute in the current system, may use mounted VHD drive letter which will not be valid later
#   softwareKey = either HKLM:\Software\Microsoft\Windows\..\Run or HKLM:\SevecekMountedSoftware\Microsoft\..\Run
#   onlineRootDrive = if specified, such as C:\ of the latter online root drive at time the scripts run
#   onlinePath = if specified, the absolute path of the script folder in the running online system
#
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBG ('Check if the target folder already exists: {0} | {1}' -f $path, (Test-Path -Literal $path))
  if (Test-Path -Literal $path) {

    DBG ('The target path already exists, get its item to be able to rename')
    DBGSTART
    $renameExistingPathItem = Get-Item $path
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
        
    [string] $renameExistingPathTo = [IO.Path]::ChangeExtension($renameExistingPathItem.Name, ($renameExistingPathItem.LastWriteTime.ToString('yyyyMMddHHmmss')))

    DBG ('The target path already exists, rename: {0}' -f $renameExistingPathTo)
    DBGSTART
    Rename-Item $renameExistingPathItem.FullName $renameExistingPathTo
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
  }

  Create-NTFSFolderAndShare $path $null 'X$BUILTIN\Users' -parentDaclIfCreating 'F$Administrators|M$Authenticated Users|R$OWNER RIGHTS'
  
  DBG ('Copy !Sevecek-Buildup contents into the target folder: {0} | {1}' -f $buildupSrc, $path)
  Run-Process 'xcopy' ('"{0}" "{1}" /E /C /Q /H /R' -f $buildupSrc, $path)


  $outputFolderPath = Join-Path $path $global:adlabOutputFolder
  DBG ('Make Output-vXX writable for BUILTIN\Users: {0}' -f $outputFolderPath)

  if (-not (Test-Path $outputFolderPath)) {

    DBG ('Output folder does not exist. Create: {0}' -f $outputFolderPath)
    DBGSTART
    New-Item -Path $outputFolderPath -ItemType Directory | Out-Null
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
  }
 
  Apply-NtfsDacl $outputFolderPath 'M$BUILTIN\Users' $true

  
  DBG ('Make other subfolders writable for BUILTIN\Users: #{0} | {1}' -f (Get-CountSafe $global:sevecekBuildupWritableFolders), ($global:sevecekBuildupWritableFolders -join ','))
  if ((Get-CountSafe $global:sevecekBuildupWritableFolders) -gt 0) {

    foreach ($oneWritableFolder in $global:sevecekBuildupWritableFolders) {

      $oneWritableFolderPath = Join-Path $path $oneWritableFolder
      DBG ('One additional writable folder: {0} | {1}' -f $oneWritableFolder, $oneWritableFolderPath)
      DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $oneWritableFolderPath }
      DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path -Literal $oneWritableFolderPath) }
      Apply-NtfsDacl $oneWritableFolderPath 'M$BUILTIN\Users' $true
    }
  }


  # Note: this shit is necessary due to the 2012 Hyper-V behavior
  #       when it does not update BIOS time on every boot as against the 2008R2 version
  $userParamsPath = Join-Path $path $global:libCommonDefaultXmlConfig
  $phaseIdPath = Join-Path $path '!phaseId.xml'
  $sourceBuildupMain = Join-Path $path 'buildup-main.ps1'
  DBG ('The target paths: params = {0} | phaseId = {1} | main = {2}' -f $userParamsPath, $phaseIdPath, $sourceBuildupMain)
  DBGIF $MyInvocation.MyCommand.Name { -not (Test-path $userParamsPath) }
  DBGIF $MyInvocation.MyCommand.Name { -not (Test-path $phaseIdPath) }
  DBGIF $MyInvocation.MyCommand.Name { -not (Test-path $sourceBuildupMain) }

  DBG ('Time update for {0} to overcome Hypervisor 2012 BIOS time shit: {1}' -f $global:libCommonDefaultXmlConfig, $userParamsPath)
  DBGSTART
  (Get-Item $userParamsPath).LastWriteTime = (Get-Date).AddDays(-1)
  #Remove-Item "$letter\Windows\TEMP\$global:sevecekBuildupRoot\$global:libCommonDefaultExpandedXmlConfig" -Force
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND

  if (Test-Path $phaseIdPath) {

    DBG ('Update phaseId with installation media type: iso = {0} | phaseId = {1}' -f $installingFromISO, $phaseIdPath)
    DBGSTART
    $localPhaseConfig = Load-XmlSafe $phaseIdPath #[xml] (cat $phaseIdPath)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBGIF $MyInvocation.MyCommand.Name { Is-Null $localPhaseConfig }
        
    if (Is-NonNull $localPhaseConfig) {

      if ($installingFromISO) {

        DBG ('Installing from ISO')
        Set-XmlAttribute $localPhaseConfig.sevecekBuildup.media source 'iso'

      } else {

        DBG ('Installing from baseVHD')
        Set-XmlAttribute $localPhaseConfig.sevecekBuildup.media source 'baseVHD'
      }

      DBG ('Update defaultAdminPwd in phaseConfig: {0} | {1}' -f $localPhaseConfig.sevecekBuildup.login.pwd, $defaultAdminPwd)
      DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $defaultAdminPwd }
      Set-XmlAttribute $localPhaseConfig.sevecekBuildup.login pwd $defaultAdminPwd

      DBG ('Save phase config into the distribution folder')
      DBGSTART
      Save-XmlSafe $localPhaseConfig $phaseIdPath -doNotCheckRootDir $true
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
    }
  }


  DBG ('Should install into registry: {0}' -f (Is-ValidString $softwareKey))

  if (Is-ValidString $softwareKey) {

    #DBG ('Set the registry values to allow PowerShell script execution and RunOnce')

    $psRoot = Join-Path $softwareKey 'Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell'
    DBG ('PowerShell registry key: exists = {0} | {1}' -f (Test-Path $psRoot), $psRoot)

    if (-not (Test-Path $psRoot)) {

      DBG ('Microsoft.PowerShell key does not exist. Create first: {0}' -f $psRoot)  
      [void] (New-Item -Path $psRoot -ItemType Registry -Force -EV er -EA SilentlyContinue)
      DBGER $MyInvocation.MyCommand.Name $er
    }

    DBG ('Current PowerShell execution policy: {0}' -f (Get-Item $psRoot).GetValue('ExecutionPolicy'))

    #DBG ('Set ExecutionPolicy value to RemoteSigned')
    #Set-ItemProperty -Path $psRoot -Name ExecutionPolicy -Value RemoteSigned -EV er -EA SilentlyContinue | Out-Null
    #DBGER $MyInvocation.MyCommand.Name $er

    DBG ('Register our SevecekBuildupRunOnce script')
    #Set-ItemProperty -Path "HKLM:\software\Microsoft\Windows\CurrentVersion\Run" -Name SevecekBuildupRunOnce -Value '"%SYSTEMROOT%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "& \"$env:SYSTEMROOT\TEMP\$global:sevecekBuildupRoot\buildup-main.ps1\""' -Type ExpandString -Force | Out-Null
    # Note: we must go into Run key, instead of RunOnce, because RunOnce is started synchronously and we would
    #       run as the first thing after the sysprep, which means that not all initializations would be done yet
    #       Run key is in the other hand started asynchronously which makes us work fine
    $runKey = Join-Path $softwareKey 'Microsoft\Windows\CurrentVersion\Run'

    if ((Is-ValidString $onlineRootDrive) -and (Is-ValidString $onlinePath)) {

      DBG ('Will use future online paths instead of the current')
      $powerShellExe = Join-Path $onlineRootDrive 'Windows\System32\WindowsPowerShell\v1.0\powershell.exe'
      $buildupMain = Join-Path $onlinePath 'buildup-main.ps1'
      # we cannot test existance because of the paths will be valid only when the mounted image is online

    } else {

      DBG ('Will use current absolute paths')

      $powerShellExe = Join-Path $PSHOME 'PowerShell.exe'
      DBGIF $MyInvocation.MyCommand.Name { -not (Test-path $powerShellExe) }

      $buildupMain = Join-Path $path 'buildup-main.ps1'
      DBGIF $MyInvocation.MyCommand.Name { -not (Test-path $buildupMain) }
    }
    
    DBG ('Run key: {0}' -f $runKey)
    DBG ('PowerShell exe path: {0}' -f $powerShellExe)
    DBG ('Buildup-Main.ps1 path: {0}' -f $buildupMain)

    DBGSTART
    [void] (Set-ItemProperty -Path $runKey -Name SevecekBuildupRunOnce -Value ('"{0}" -NoLogo -NoProfile -ExecutionPolicy Bypass -NoExit -File "{1}"' -f $powerShellExe, $buildupMain) -Type ExpandString)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
  }
}

function global:Redirect-TempToOutput ([string] $anyNameSuffix)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  $scriptTempBaseName = 'ScriptTemp'
  $scriptTempName = '{0}-{1}-pid{2:X4}' -f $scriptTempBaseName, ([DateTime]::Now).ToString('yyyyMMddHHmmss'), ([System.Diagnostics.Process]::GetCurrentProcess().Id)

  if (Is-ValidString $anyNameSuffix) {

    $scriptTempName = '{0}-{1}' -f $scriptTempName, $anyNameSuffix
  }

  $scriptTemp = Get-DataFileApp $scriptTempName -noExtension $true
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $scriptTemp }
  DBGIF $MyInvocation.MyCommand.Name { Test-Path -Literal $scriptTemp }
  DBGIF ('Possible recurency in script temp path: {0}' -f $scriptTemp) { $scriptTemp -match ('{0}.+{0}\-\d{{14}}' -f $scriptTempBaseName) }
  DBGIF ('Very long script temp path: #{0} | {1}' -f $scriptTemp.Length, $scriptTemp) { $scriptTemp.Length -gt 100 }

  if ((Is-ValidString $scriptTemp) -and (-not (Test-Path -Literal $scriptTemp))) {

    DBG ('Creating the new script temp folder: {0}' -f $scriptTemp)
    DBGSTART
    New-Item -Path $scriptTemp -ItemType Directory -Force | Out-Null
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path -Literal $scriptTemp) }

    if (Test-Path -Literal $scriptTemp) {

      DBG ('Setting security on the new script temp to Authenticated Users: {0}' -f $scriptTemp)
      Apply-NtfsDacl -ntfs $scriptTemp -dacl 'F$Authenticated Users'

      DBG ('Redirecting the environment TEMP folders to the new script temp: {0}' -f $scriptTemp)
      $env:TEMP = $scriptTemp
      $env:TMP = $scriptTemp

      DBGIF $MyInvocation.MyCommand.Name { [IO.Path]::GetTempPath().Trim('\') -ne $env:TEMP.Trim('\') }
    }
  }
}

function global:Get-VMICVersion ()
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  if ($global:runningInHyperV) {

    $vmICDriver = "$env:SystemRoot\System32\Drivers\vmbus.sys"
    DBG ("Hyper-V integration components HVIC driver exists: {0}" -f (Test-Path $vmICDriver))

    [string] $global:vmICVersion = $null
    [double] $global:vmICVersionNumber = 0

    if (Test-Path $vmICDriver) {

      $global:vmICVersion = (Get-ChildItem $vmICDriver).VersionInfo.ProductVersion
      DBG ("Hyper-V Integration components version: {0}" -f ((Get-ChildItem $vmICDriver).VersionInfo | Format-List * | Out-String))
      $global:vmICVersionNumber = Get-OSVersionNumber $global:vmICVersion
    }
  
  } else {

    DBG ('We are not running in Hyper-V, will not determine Hyper-V Integration Components (HVIC) version')
  }
}


function global:Get-FirstPathFromWildcard ([string] $wildCardPath, [bool] $doNotAssertNonExisting)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  [string] $firstFoundPath = $null

  DBG ('Some such actual paths exist: {0} | {1}' -f (Test-Path $wildCardPath), $wildCardPath)

  if (Test-Path $wildCardPath) {

    DBG ('Obtain list of items that meet the wildcard spec.')
    DBGSTART
    $foundItems = Get-Item $wildCardPath
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    DBG ('Found items: {0}' -f (Get-CountSafe $foundItems))

    if ((Get-CountSafe $foundItems) -ge 1) {

      if ((Get-CountSafe $foundItems) -eq 1) {

        $firstFoundPath = $foundItems.FullName
      
      } else {

        $firstFoundPath = $foundItems[0]
      }
    }
  }


  DBGIF ('Didn find any path from wildcard: {0}' -f $wildCardPath) { (-not $doNotAssertNonExisting) -and (Is-EmptyString $firstFoundPath) }
  DBGIF $MyInvocation.MyCommand.Name { (-not $doNotAssertNonExisting) -and (Is-ValidString $firstFoundPath) -and (-not (Test-Path $firstFoundPath)) }

  DBG ('First found path: {0}' -f $firstFoundPath)

  return $firstFoundPath
}


function global:Find-MarkedVolumes()
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  $global:installMediaVolume = Find-MarkedVolume $global:installSourceMedia 3
  DBG ('Install media volume: {0}' -f $installMediaVolume)

  $global:toolsISOVolume = Find-MarkedVolume $global:toolsISO 5
  DBG ('Tools ISO volume: {0}' -f $toolsISOVolume)

  [string] $global:installISOVolume = ''
  DBG ('Were we installed from ISO: {0}' -f $global:phaseCfg.sevecekBuildup.media.source)

  if ($global:phaseCfg.sevecekBuildup.media.source -eq 'iso') {

    if ($global:thisOSVersionNumber -lt 6) {

      DBG ('OS 5.x was installed from an ISO file, going to find the drive')
      $global:installISOVolume = Find-MarkedVolume 'i386\ntfs.sys' 5 -findAllWithWildcard:$false -fileVersionWildcard "$global:thisOSVersion.*"
      DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $global:installISOVolume }
      DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path (Join-Path $global:installISOVolume i386\winnt.exe)) }
      DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path (Join-Path $global:installISOVolume setup.exe)) }

    } else {

      DBG ('OS 6.x was installed from an ISO file, going to find the drive')
      $global:installISOVolume = Find-MarkedVolume 'sources\setup.exe' 5 -findAllWithWildcard:$false -fileVersionWildcard "$global:thisOSVersion.*"
      DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $global:installISOVolume }
      DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path (Join-Path $global:installISOVolume sources\install.wim)) }
      DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path (Join-Path $global:installISOVolume sources\boot.wim)) }
      DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path (Join-Path $global:installISOVolume boot)) }
      DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path (Join-Path $global:installISOVolume bootmgr)) }
    }
  }
}


function global:Get-NICsListFromXmlConfig ([System.Xml.XmlElement] $vmConfig = $global:vmConfig)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { Is-Null $vmConfig }

  [System.Collections.ArrayList] $nicList = @()

  if (Is-ValidString $vmConfig.net.nicNames) {

    $nicNames = Split-MultiValue $vmConfig.net.nicNames
    $nicIPs = Split-MultiValue $vmConfig.net.ip
    $nicMasks = Split-MultiValue $vmConfig.net.ipMask
    $nicGWs = Split-MultiValue $vmConfig.net.ipGW
    $nicRouters = Split-MultiValue $vmConfig.net.routing
    $allDNSs = Split-MultiValue $vmConfig.net.ipDNS

    if (Is-ValidString $vmConfig.vm.nics) {

      # Note: if the machine is either physical, or it is intended for
      #       manual injection into an existing VM, then the HV NICs might
      #       not be specified at all

      $hvNetworks = Split-MultiValue $vmConfig.vm.nics
      DBGIF ('Weird VM vs. HV NICs: vmNICs = {0} | {1} | hvNICs = {2} | {3}' -f $nicNames.Count, ($nicNames -join ','), $hvNetworks.Count, ($hvNetworks -join ',')) { $hvNetworks.Count -ne $nicNames.Count }
    }

    DBG ('Found NICs: {0}' -f $nicNames.Count)
    DBGIF $MyInvocation.MyCommand.Name { (-not (($nicNames.Count -eq $nicIPs.Count) -and ($nicNames.Count -eq $nicMasks.Count) -and ($nicNames.Count -eq $nicGWs.Count)))}

    for ($i = 0; $i -lt $nicNames.Count; $i ++) {

      DBG ("NIC to be defined: {0}: {1}" -f $i, $nicNames[$i])
    
      $forwarding = ($nicRouters.Count -gt $i) -and (Parse-BoolSafe (Strip-ValueFlags $nicRouters[$i]))
    
      $natType = $null
      if ($forwarding) { $natType = Get-ValueFlags $nicRouters[$i] }
    

      DBG ('Parse client DNS server list: #{0} = {1}' -f (Get-CountSafe $allDNSs), ($allDNSs -join ','))

      $nicDNSsExplicit = ''
      $nicDNSsImplicit = ''

      if ((Get-CountSafe $allDNSs) -gt 0) {

        foreach ($oneDNS in $allDNSs) {

          if ((Get-ValueFlags $oneDNS) -eq ($i + 1)) {
      
            $nicDNSsExplicit = Add-MultiValue $nicDNSsExplicit (Strip-ValueFlags $oneDNS) $true

          } elseif (-not (Has-ValueFlags $oneDNS)) {
      
            $nicDNSsImplicit = Add-MultiValue $nicDNSsImplicit (Strip-ValueFlags $oneDNS) $true
          }
        }
      }

      [string] $nicDNSs = ''
      if (Is-ValidString $nicDNSsImplicit) { 

        DBG ('Implicit DNS server list: {0}' -f $nicDNSsImplicit)
        $nicDNSs = Add-MultiValue $nicDNSs (Split-MultiValue $nicDNSsImplicit) $true
      }

      if (Is-ValidString $nicDNSsExplicit) {
       
        DBG ('Explicit DNS server list: {0}' -f $nicDNSsExplicit)
        $nicDNSs = Add-MultiValue $nicDNSs (Split-MultiValue $nicDNSsExplicit) $true
      }


      $oneNewNIC = New-Object PSCustomObject
      Add-Member -InputObject $oneNewNIC -MemberType NoteProperty -Name name -Value $nicNames[$i]
      Add-Member -InputObject $oneNewNIC -MemberType NoteProperty -Name IPs -Value $nicIPs[$i]
      Add-Member -InputObject $oneNewNIC -MemberType NoteProperty -Name masks -Value $nicMasks[$i]
      Add-Member -InputObject $oneNewNIC -MemberType NoteProperty -Name gateways -Value $nicGWs[$i]
      Add-Member -InputObject $oneNewNIC -MemberType NoteProperty -Name dnss -Value $nicDNSs
      Add-Member -InputObject $oneNewNIC -MemberType NoteProperty -Name forwarding -Value $forwarding
      Add-Member -InputObject $oneNewNIC -MemberType NoteProperty -Name natType -Value $natType

      if ($hvNetworks.Count -ge $i) {

        Add-Member -InputObject $oneNewNIC -MemberType NoteProperty -Name hvNetwork -Value $hvNetworks[$i]

      } else {

        Add-Member -InputObject $oneNewNIC -MemberType NoteProperty -Name hvNetwork -Value ''
      }

      [void] $nicList.Add($oneNewNIC)
    }
  }

  DBG ('XML configuration defines the following NICs: {0}' -f ($nicList | ft | Out-String))

  return (, $nicList)
}


function global:Configure-NICs ()
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  $nicList = Get-NICsListFromXmlConfig

  if (Get-CountSafe $nicList) {

    $routingAlreadyInstalled = $false

    foreach ($oneNIC in $nicList) {
  
      if ($oneNIC.forwarding -and (-not $routingAlreadyInstalled)) {

        # Note: mind the sharp lower operator
        if (($thisOSVersionNumber -ge 6.0) -and ($thisOSVersionNumber -lt 6.2)) {

          DBG ('Install Routing on Windows 2008 or 2008 R2')
          $restartRequired = Install-WindowsFeaturesUniversal @('NPAS-Routing') $false $installMediaVolume $global:installISOVolume
          DBGIF $MyInvocation.MyCommand.Name { $restartRequired }

        } elseif ($thisOSVersionNumber -ge 6.2) {

          DBG ('Install Routing on Windows 2012 and newer')
          $restartRequired = Install-WindowsFeaturesUniversal @('Routing') $false $installMediaVolume $global:installISOVolume
          DBGIF $MyInvocation.MyCommand.Name { $restartRequired }
        
        } else {

          DBG ('Do not need to install Routing on Windows 5.x')
        }

        $routingAlreadyInstalled = $true
      }

      Configure-NICbyName $oneNIC.name $oneNIC.IPs $oneNIC.masks $oneNIC.gateways $oneNIC.dnss $oneNIC.forwarding $oneNIC.natType
    }
  }
}


function global:Fix-DcDnsNetworkAdapters ([bool] $useAllNameServers)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  $isLocalDC = Is-LocalComputerDomainController
  DBGIF $MyInvocation.MyCommand.Name { -not $isLocalDC }

  if ($isLocalDC) {

    DBG ('Check if we do have DNS server installed')
    $dnsNamespace = Get-WMIQuerySingleObject '.' 'SELECT * FROM __Namespace WHERE Name = "MicrosoftDNS"' -namespace root
    DBG ('DNS server installed: {0}' -f (Is-NonNull $dnsNamespace))

    if (Is-NonNull $dnsNamespace) {

      $allNICs = Get-WmiQueryArray '.' 'SELECT * FROM Win32_NetworkAdapterConfiguration'
      DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $allNICs) -lt 1 }
      
      [string[]] $allNICsIPs = $allNICs | ? { Is-NonNull $_.IPAddress } | Select -Expand IPAddress
      DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $allNICsIPs) -lt 1 }

      $localDnsServer = Get-WMIQuerySingleObject '.' 'SELECT * FROM MicrosoftDNS_Server' -namespace 'root\MicrosoftDNS'
      DBG ('Local DNS server: listenIPs = {0} | serverIPs = {1}' -f ($localDnsServer.ListenAddresses -join ','), ($localDnsServer.ServerAddresses -join ','))
      DBGIF $MyInvocation.MyCommand.Name { Is-Null $localDnsServer }
      DBGIF $MyInvocation.MyCommand.Name { -not $localDnsServer.DsAvailable }
      DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $localDnsServer.ServerAddresses) -lt 1 }


      [Collections.ArrayList] $localDnsServerIPs = @()

      if ((Get-CountSafe $localDnsServer.ListenAddresses) -gt 0) {

        foreach ($oneLocalDnsServerListenAddress in $localDnsServer.ListenAddresses) {

          DBG ('DNS server listens on only specific listen IPs. Adding: {0}' -f $oneLocalDnsServerListenAddress)
          DBGIF $MyInvocation.MyCommand.Name { -not (Contains-Safe $allNICsIPs $oneLocalDnsServerListenAddress) }

          Add-ListUnique ([ref] $localDnsServerIPs) $oneLocalDnsServerListenAddress
        }

      } else {

        foreach ($oneLocalDnsServerServerAddress in $localDnsServer.ServerAddresses) {

          DBG ('DNS server listens on all physical IPs. Adding: {0}' -f $oneLocalDnsServerServerAddress)
          Add-ListUnique ([ref] $localDnsServerIPs) $oneLocalDnsServerServerAddress
        }

        DBG ('DNS server listens on all physical IPs. Adding: 127.0.0.1')
        Add-ListUnique ([ref] $localDnsServerIPs) '127.0.0.1'
      }


      DBG ('We will update the local DNS client resolver IPs with the following local DNS server IPs: #{0} | {1}' -f $localDnsServerIPs.Count, ($localDnsServerIPs -join '.'))

      if ($localDnsServerIPs.Count -gt 0) {

        foreach ($oneNIC in $allNICs) {

          if (((Get-CountSafe $oneNIC.DNSServerSearchOrder) -gt 0) -and ((Get-CountSafe $oneNIC.IPAddress) -gt 0)) {
        
            DBG ('One NIC possible for DNS IPs fixup found: {0} | {1} | {2} | {3}' -f $oneNIC.Description, ($oneNIC.IPAddress -join ','), ($oneNIC.DNSServerSearchOrder -join ','), $oneNIC.MacAddress)
            [Collections.ArrayList] $newDnsServerSearchOrder = $oneNIC.DNSServerSearchOrder

            foreach ($oneLocalDnsServerIP in $localDnsServerIPs) {

              DBG ('Adding the local DNS server IP into the NIC settings: toAdd = {0} | currentList = {1}' -f $oneLocalDnsServerIP, ($newDnsServerSearchOrder -join ','))
              Add-ListUnique ([ref] $newDnsServerSearchOrder) $oneLocalDnsServerIP
            }

            DBG ('Will update the DNS server search order with the following: {0}' -f ($newDnsServerSearchOrder -join ','))
            DBGSTART
            $wmiRs = $oneNIC.SetDNSServerSearchOrder($newDnsServerSearchOrder)
            DBGER $MyInvocation.MyCommand.Name $error
            DBGEND
            DBGWMI $wmiRs
          }
        }
      }
    }
  }
}


function global:Update-HyperVIntegration ()
{
  [bool] $icUpdated = $false

  if ($global:runningInHyperV) {

    #===============
    DBG ("Updating Hyper-V Integration Services (HVIC).")
  
    $hostICVersion = $phaseCfg.sevecekBuildup.host.icVersion
    DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $hostICVersion }

    $updatesAvailable = Get-ChildItem -Path "$global:rootDir\ForeignBinaries\HyperVIntegration" | ? { ($_.psIsContainer -eq $true) -and ($_.Name -like '*.*.*.*') } | % { $_.Name } #| Sort-Object
    DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $updatesAvailable) -lt 1 }
  

    [double] $vmICVersionNumber = 0
  
    if (Is-ValidString $global:vmICVersion) {
  
      $vmICVersionNumber = Get-OSVersionNumber $global:vmICVersion -includeBuild $true
    }

    [double] $hostICVersionNumber = Get-OSVersionNumber $hostICVersion -includeBuild $true

    DBG ('Our IC version: {0} | # = {1}' -f $global:vmICVersion, $vmICVersionNumber)
    DBG ('Host IC version: {0} | # = {1}' -f $hostICVersion, $hostICVersionNumber)
    DBG ('Found IC updates: {0}' -f (Format-MultiValue $updatesAvailable))

  
    [string] $bestUpdate = ''
    [double] $bestUpdateNumber = 0

    $updatesAvailable | % { 
  
      $oneUpdateNumber = Get-OSVersionNumber $_ -includeBuild $true
      if (($oneUpdateNumber -le $hostICVersionNumber) -and ($oneUpdateNumber -gt $bestUpdateNumber)) { 
    
        $bestUpdate = $_
        $bestUpdateNumber = $oneUpdateNumber
      }
    }

    DBG ('Best IC update version detected: {0} | # = {1} | willUpdate = {2}' -f $bestUpdate, $bestUpdateNumber, ($bestUpdateNumber -gt $vmICVersionNumber))
    DBGIF $MyInvocation.MyCommand.Name { $bestUpdate -eq '' }
    DBGIF $MyInvocation.MyCommand.Name { $bestUpdateNumber -eq 0 }
    
    if (($global:thisOSVersionNormal -eq '5.2srv') -and (Is-ValidString $global:vmICVersion)) {

      DBGSTART
      $winDriverFx = $null
      $winDriverFx = Get-Service wdf01000
      DBGEND # Note: just ignore non-present errors

      # Note: VMIC HVIC Hyper-V integration services log files:
      #    C:\Windows\VMGuestSetup.log 
      #    C:\Windows\VMGcoInstall.log
      DBGIF $MyInvocation.MyCommand.Name { (Is-NonNull $winDriverFx) -and ($winDriverFx.Status -ne 'Running') }
    }

    if ($bestUpdateNumber -gt $vmICVersionNumber) {
  
      Run-Process "$global:rootDir\ForeignBinaries\HyperVIntegration\$bestUpdate\support\x86\setup.exe" '/quiet /norestart'

      $icUpdated = $true
    }
  
  } else {

    DBG ('Not running in Hyper-V, not updating the HVIC components')
  }

  DBG ('HVIC actually updated: {0}' -f $icUpdated)
  return $icUpdated
}


function global:Define-JoinerCredentials ([string] $joinerLogin, [string] $joinerDomain, [string] $joinerPwd, [string] $joinerFullLogin)
{
  $outCred = New-Object PSObject

  $outCred | Add-Member -MemberType NoteProperty -Name Login -Value $joinerLogin
  $outCred | Add-Member -MemberType NoteProperty -Name Domain -Value $joinerDomain
  $outCred | Add-Member -MemberType NoteProperty -Name Pwd -Value $joinerPwd
  $outCred | Add-Member -MemberType NoteProperty -Name FullLogin -Value $joinerFullLogin

  DBG ('Joiner credentials: {0} @ {1} | {2}' -f $outCred.Login, $outCred.Domain, $outCred.FullLogin)
  DBGIF $MyInvocation.MyCommand.Name { (Is-ValidString $outCred.Domain) -and (Is-LocalDomain $outCred.Domain $true) }

  return $outCred
}


function global:Get-JoinerCredentials ([System.Xml.XmlElement] $vmXml)
# Note: in case we are a DC and we are the first DC in the forest
#       the joiner account might not be valid already. 
#       If that is the case, we return nothing
#
# Note: There are actually two scenarios - when we are obtaining joiner credentials for ourselves (joining a domain)
#       or when we determine them for another machine (such as when prestaging a computer account)
#
#       There are three types of machines from this point of view:
#       - first DC of a forest - its joiner cred is problematic,
#                                if we select the builtin-admin, it would change over time
#                                if we select the new domain-admin, it is not mandatory and we cannot be sure to pick him
#       - domain members - their joiner cred is stable
#       - other DCs in a forest - all such DCs will be installed only after their first forest DC is finished, so
#                                 even their joiner account will be stable
#
#       When do we need the joiner credentials?
#       - I myself to join me into a domain to wait for the first DC
#       - I myself to lookup machine state when waiting - first forest DC does not wait, it can be determined with its @new value
#       - I myself to mark myself finished - first forest DC finishes itself simply with its current logon identity
#       - DC to prestage others
#
#       All of the previous thoughts lead me to the conclusion that the only necessary exception is
#       if I am the first forest DC and I determine my own creds - then use current user instead of the defined one
{
  DBG ('Determining joiner credentials: {0}' -f $vmXml.hostName)
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $vmXml.hostName }
  
  $joinerLogin = $null
  $joinerDomain = $null
  $joinerPwd = $null
  $joinerFullLogin = $null
  
  if (Is-NonNull $vmXml) {

    if (Is-ValidString $vmXml.domain.login) {

      DBG ('Domain element specifies explicit credentials for joining domain or a forest for first subdomain DCs')

      $joinerLogin = $vmXml.domain.login
      $joinerPwd = $vmXml.domain.pwd
      $joinerDomain = $vmXml.domain.join
    }

    else {

      DBG ('Try selecting an application install account for joiner credentials')

      $firstApp = $vmXml.SelectSingleNode('./*/app[@iLogin!="" and @iDomain!="." and @iDomain!=""]')
      
      if (Is-NonNull $firstApp) {

        DBG ("Will use joiner credentials from app install account: {0}" -f $firstApp.psbase.ParentNode.name)
        $joinerLogin = $firstApp.iLogin
        $joinerPwd = $firstApp.iPwd
        $joinerDomain = $firstApp.iDomain

      } else {

        DBG ('No app install account found, try seeking for an app admin account')
        $firstApp = $vmXml.SelectSingleNode('./*/app[@aLogin!="" and @iDomain!="." and @iDomain!=""]')
      
        if (Is-NonNull $firstApp) {

          DBG ("Will use joiner credentials from app admin account: {0}" -f $firstApp.psbase.ParentNode.name)
          $joinerLogin = $firstApp.aLogin
          $joinerPwd = $firstApp.aPwd
          $joinerDomain = $firstApp.iDomain
        }
      }
    }
  }


######################
# OBSOLETE, replaced with the @new logic for root-domain first DCs
#
  # Note: in case we are a DC and we are the first DC in the forest
  #       the joiner account might not be valid already. Although there are
  #       other situations that meet the following simple condition, such as 
  #       a DC which tries to determine joiner credentials for other machines
  #       in order to prestage their accounts, and although the subsequent existance
  #       test is redundant in such a case, it brings no big overhead and we
  #       can safely do it in order to make the code as simple and robust as possible
#  if ((($global:thisOSRole -eq 'BDC') -or ($global:thisOSRole -eq 'PDC')) -and (Is-ValidString $joinerDomain) -and ($global:thisComputerDomain -eq $joinerDomain)) {
#
#    DBG ('This computer is a DC and we are selecting joiner credentials from the same domain')
#
#    $adsiObjPath = 'WinNT://./{0},user' -f $joinerLogin
#    DBG ('Test if the account is yet valid: {0}' -f $adsiObjPath)
#
#    DBGSTART
#    $adsiObj = $null
#    $adsiObj = [ADSI] $adsiObjPath
#    DBG ('Joiner account object ADSI path opened: {0}' -f $adsiObj.Path)
#    DBGEND
#
#    if (Is-ValidString $adsiObj.Path) {
#
#      $adsiObjDisabled = (([int] $adsiObj.UserFlags.Value) -band 2) -eq 2
#    }
#
#    if ((Is-EmptyString $adsiObj.Path) -or $adsiObjDisabled) {
#
#      $joinerLogin = $null
#      $joinerDomain = $null
#      $joinerPwd = $null
#      $joinerFullLogin = $null
#    }
#  }

  # If we are forest rootdomain first DC, we just return nothing
  # because whatever our possible joiner credentials are, they might not be valid yet
  # or even disabled anymore - as mentioned in the previous logic, although technologically obsolete
  # the note is logically still correct
  if (Is-FirstForestDC $vmXml)
  {
    DBG ('This is the first forest DC, no valid joiner credentials possible')

    $joinerLogin = $null
    $joinerDomain = $null
    $joinerPwd = $null
    $joinerFullLogin = $null
  }
   

  if (Is-ValidString $joinerLogin) {
  
    $joinerFullLogin = '{0}@{1}' -f $joinerLogin, $joinerDomain
  
  }

  DBGIF ('Could not establish any valid joiner credentials - might not be a problem when rebuilding only some machines: {0}' -f $vmXml.hostName) { (Is-EmptyString $joinerFullLogin) -and (-not (Is-FirstForestDC $vmXml)) }
  
  $outCred = Define-JoinerCredentials $joinerLogin $joinerDomain $joinerPwd $joinerFullLogin
 
  return $outCred
}


function global:Assert-HyperVClientConfigFile ()
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  $cfgFile = "$env:AppData\Microsoft\Windows\Hyper-V\Client\1.0\clientsettings.config"

  DBG ('Testing whether config file exist: {0} | {1}' -f $cfgFile, (Test-Path $cfgFile))

  if (-not (Test-Path $cfgFile)) {
  
    DBG ('Config file does not exist. Copying the default one.')
    DBGSTART
    [void] (New-Item -Path $cfgFile -ItemType File -Force -EA SilentlyContinue)
    Copy-Item -Path (Join-Path $global:rootDir 'User-Env\Hyper-V-clientsettings.config') -Destination $cfgFile -Force -EA SilentlyContinue
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
  }

  return $cfgFile
}


function global:Update-HyperVKeyboard ([string] $adjustment)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  $cfgFile = Assert-HyperVClientConfigFile

  if (Test-Path $cfgFile) {
  
    DBG ('Loading XML: {0}' -f $cfgFile)
    DBGSTART
    $cfg = Load-XmlSafe $cfgFile #[XML] (Get-Content $cfgFile -EA SilentlyContinue)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    DBG ('Adjust keyboard client setting: {0}' -f $adjustment)
    DBGSTART
    $cfg.SelectSingleNode('//setting[@name="VMConnectKeyboardOption"]').value = $adjustment
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    DBG ('Saving the modified XML.')
    DBGSTART
    Save-XmlSafe $cfg $cfgFile -doNotCheckRootDir $true
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
  }
}


function global:Change-HyperVClientESM ([bool] $enable)
{
<#

    
        
            False
        
#>
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  $cfgFile = Assert-HyperVClientConfigFile

  if (Test-Path $cfgFile) {
  
    DBG ('Loading XML: {0}' -f $cfgFile)
    DBGSTART
    $cfg = Load-XmlSafe $cfgFile #[XML] (Get-Content $cfgFile -EA SilentlyContinue)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    DBG ('Adjust ESM client setting: {0}' -f $enable)
    DBGSTART
    $esmElement = $null
    $esmElement = $cfg.SelectSingleNode('//setting[@name="VMConnectUseEnhancedMode"]')
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    if (Is-Null $esmElement) {

      Append-XmlElement $cfg.configuration.'Microsoft.Virtualization.Client.ClientVirtualizationSettings' setting @{ 'name' = 'VMConnectUseEnhancedMode'; 'type' = 'System.Boolean' }

      DBGSTART
      $esmElement = $null
      $esmElement = $cfg.SelectSingleNode('//setting[@name="VMConnectUseEnhancedMode"]')
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND

      Append-XmlElement $esmElement value @{}
    }

    DBGIF $MyInvocation.MyCommand.Name { Is-Null $esmElement }
    DBGSTART
    $esmElement.Value = $enable.ToString()
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    DBG ('Saving the modified XML.')
    DBGSTART
    Save-XmlSafe $cfg $cfgFile -doNotCheckRootDir $true
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
  }
}


function global:Show-TrayNicIcons ()
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Network\{4D36E972-E325-11CE-BFC1-08002BE10318}').Class -ne 'Net' }

  DBGSTART
  $networkInterfaces = Get-ChildItem "HKLM:\SYSTEM\CurrentControlSet\Control\Network\{4D36E972-E325-11CE-BFC1-08002BE10318}\{$global:likeGUID}" -EA SilentlyContinue
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND

  DBG ('Found network interfaces: {0}' -f (Get-CountSafe $networkInterfaces))

  if ((Get-CountSafe $networkInterfaces) -gt 0) {

    foreach ($oneNIC in $networkInterfaces) {

      $oneNICkey = "$($oneNIC.PSPath)\Connection"
      $nicProps = (Get-ItemProperty $oneNICkey)
      DBG ('Network interface: {0} | showIcon = {1} | key = {2}' -f $nicProps.Name, $nicProps.ShowIcon, $oneNICkey)

      DBG ('Set ShowIcon = DWORD = 1')
      Set-RegistryValue $oneNICkey ShowIcon 1 DWord
    }
  }
}


function global:Install-ScheduledJob ([System.Xml.XmlElement] $vmConfig, [string] $appTag, [string] $sourceScriptName, [string] $sourceFolder, [string] $targetFileNameCustomization, [string] $argReplacement, [string] $dacl, [bool] $replaceDACL)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $vmConfig }

  [string] $installedScriptPath = ''

  $sourceFolderAbsolute = Join-Path $global:rootDir $sourceFolder
  DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path $sourceFolderAbsolute) }

  if ((Is-NonNull $vmConfig) -and (Test-Path $sourceFolderAbsolute)) {

    [System.Xml.XmlElement] $fsJobsNode = $null
    
    DBG ('Try finding the FS jobs node in the local application config')
    DBGSTART
    $fsJobsNode = $vmConfig.$appTag.SelectSingleNode('./fs[@instance="local" and @appTag="jobs"]')
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    if (Is-Null $fsJobsNode) {

      DBG ('FS jobs node not found locally, try the whole VM config')
      DBGSTART
      $fsJobsNode = $vmConfig.SelectSingleNode('./fs[@instance="local" and @appTag="jobs"]')
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
    }

    DBGIF $MyInvocation.MyCommand.Name { Is-Null $fsJobsNode }
    if (Is-NonNull $fsJobsNode) {

      $fsJobsPath = Resolve-ClientFsPath $fsJobsNode
      DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path $fsJobsPath) }

      if (Test-Path $fsJobsPath) {
      
        if (Is-EmptyString $targetFileNameCustomization) {

          $targetFileName = $sourceScriptName

        } else {

          $targetFileName = '{0}-{1}' -f $sourceScriptName, $targetFileNameCustomization
        }

        DBG ('Prepare the script to be published: source = {0} | srcPath = {1} | target = {2}' -f $sourceScriptName, $sourceFolderAbsolute, $targetFileName)
        $scriptFileTemp = Get-DataFileApp $targetFileName $null '.bat'
        Replace-ArgumentsInFile (Join-Path $sourceFolderAbsolute "$sourceScriptName.bat") $argReplacement $scriptFileTemp ASCII

        [System.Collections.ArrayList] $allTargetItemsCopied = @()
        DBG ('Copy the expanded script file into the FS jobs target: {0} | {1} | {2}' -f $scriptFileTemp, $fsJobsPath, $targetFileName)
        DBGSTART
        # Note: do not -Force as we should know about any rewrites or orther failures
        Copy-Item -Path $scriptFileTemp -Destination (Join-Path $fsJobsPath "$targetFileName.bat") -PassThru | % { [void] $allTargetItemsCopied.Add($_.FullName) }
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND

        [System.Collections.ArrayList] $otherFoundItemsToCopy = @()
        DBG ('Check if any other item should be copied: {0} | {1}' -f $sourceFolderAbsolute, $sourceScriptName)
        DBGSTART
        Get-ChildItem (Join-Path $sourceFolderAbsolute "$sourceScriptName.*") -Exclude *.bat | % { [void] $otherFoundItemsToCopy.Add($_) }
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND

        DBG ('Found other job items to copy: # = {0} | {1}' -f $otherFoundItemsToCopy.Count, (($otherFoundItemsToCopy | Select -Expand Name) -join ', '))
        if ($otherFoundItemsToCopy.Count -gt 0) {

          DBG ('Copy the other job items to the destination as well: {0}' -f $fsJobsPath)
          foreach ($oneOtherFoundItemToCopy in $otherFoundItemsToCopy) {

            if (Is-EmptyString $targetFileNameCustomization) {

              $oneOtherFoundItemToCopyTarget = Join-Path $fsJobsPath $oneOtherFoundItemToCopy.Name
            
            } else {

              $oneOtherFoundItemToCopyTarget = Join-Path $fsJobsPath ('{0}-{1}{2}' -f $oneOtherFoundItemToCopy.BaseName, $targetFileNameCustomization, $oneOtherFoundItemToCopy.Extension)
            }
            
            DBG ('Copy the other item: {0} | {1}' -f $oneOtherFoundItemToCopy.FullName, $oneOtherFoundItemToCopyTarget)
            DBGSTART
            Copy-Item -Path $oneOtherFoundItemToCopy.FullName -Destination $oneOtherFoundItemToCopyTarget -PassThru | % { [void] $allTargetItemsCopied.Add($_.FullName) }
            DBGER $MyInvocation.MyCommand.Name $error
            DBGEND
          }
        }

        DBG ('Target items copied overall: {0}' -f $allTargetItemsCopied.Count)
        DBGIF $MyInvocation.MyCommand.Name { $allTargetItemsCopied.Count -ne ($otherFoundItemsToCopy.Count + 1) }

        if ($allTargetItemsCopied.Count -eq ($otherFoundItemsToCopy.Count + 1)) {

          $installedScriptPath = $allTargetItemsCopied[0]

          DBG ('Do we have a custom DACL to apply: {0}' -f (Is-ValidString $dacl))

          if (Is-ValidString $dacl) {

            foreach ($oneTargetItemCopied in $allTargetItemsCopied) {

              DBG ('Apply custom ACEs into one target item: {0} | {1}' -f $dacl, $oneTargetItemCopied)
              Apply-NtfsDacl -ntfs $oneTargetItemCopied -dacl $dacl -addOnly (-not $replaceDACL)
            }
          }
        }
      }    
    }
  }

  DBG ('Returning the installed script path: {0}' -f $installedScriptPath)
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $installedScriptPath }
  DBGIF $MyInvocation.MyCommand.Name { (Is-ValidString $installedScriptPath) -and (-not (Test-Path $installedScriptPath)) }

  return $installedScriptPath
}


function global:Prelogon-User ([string] $login, [string] $domain, [string] $password, [bool] $randomDesktopColor)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  $desktopColors = @('10 59 118', '13 104 107', '70 70 70', '86 63 127', '140 98 57', '184 40 50', '132 135 28', '120 120 120', '193 64 0')
  [int] $result = 0

  if (-not $randomDesktopColor) {

    $result = Run-Process 'hostname' '' -doNotRedirOutput $true -login $login -domain $domain -password $password -returnExitCode $true
 
  } else {

    $randomColor = Get-Random $desktopColors
    
    #$runOnceResetWallPaperToSolidColor = 'reg ADD \"HKCU\Control Panel\Desktop\" /v WallPaper /t REG_SZ /d \"\" /f'
    #$runOnceRandomBackgroundColor = 'reg ADD \"HKCU\Control Panel\Colors\" /v Background /t REG_SZ /d \"{0}\" /f' -f $randomColor
    #$runOnceUpdatePerUserSystemParameters = 'rundll32 user32.dll,UpdatePerUserSystemParameters ,1 ,true'

    if ($global:thisOSVersionNumber -ge 6) {

      $transcodedWallpaper = '%APPDATA%\Microsoft\Windows\Themes\TranscodedWallpaper.*'

    } else {

      $transcodedWallpaper = '%APPDATA%\Microsoft\Internet Explorer\Wallpaper*.bmp'
    }

    #$runOnceDeleteTranscodedWallpaper = 'cmd /c del /F /Q \"{0}\"' -f $transcodedWallpaper


    
    DBG ('As long as we want to reset the wallpaper to a solid color, we must do it only after the initial DefaultUser profile customization after first full logon')

    if (Is-EmptyString $domain) {

      $userLoginWritable = Canonicalize-FileName $login
      
      # Note: the DACL must not have ContainerInherit|ObjectInherit because
      #       it applies to files which cannot have this propagation at all
      #       or we get error: "No flags can be set - parameter name: inheritance flags"
      $userDACL = ('TX${0}' -f $login)
      
    } else {
    
      # Note: in case of domain '.' the Set-ACL cannot translate the login@. format
      #       so we help here by defining it in a more suitable way
      # Note: the Set-ACL cannot translate local logins in the form of login@COMPUTER
      #       and as such the local logins must be in the COMPUTER\login form

      if (Is-LocalDomain $domain $true) {
      
        $domain = $global:thisComputerNetBIOS
        $userDACL = ('TX${0}\{1}' -f $domain, $login)

      } else {

        $userLoginWritable = Canonicalize-FileName ('{0}@{1}' -f $login, $domain)
        $userDACL = ('TX${0}@{1}' -f $login, $domain)
      }
    }

    $installedJob = Install-ScheduledJob -vmConfig $vmConfig -appTag wks -sourceScriptName sevecek-run-once-user-env-update -sourceFolder User-Env -targetFileNameCustomization $userLoginWritable -argReplacement ('cmdColor${0}|delTranscodedWalpaper${1}' -f $randomColor, $transcodedWallpaper) -dacl $userDACL


    #Run-Process 'reg' ('ADD "HKCU\Software\Microsoft\Windows\CurrentVersion\RunOnce" /v 01ResetWallPaperToSolidColor /t REG_SZ /d "{0}" /f' -f $runOnceResetWallPaperToSolidColor) -doNotRedirOutput $true -login $login -domain $domain -password $password -returnExitCode $false
    #Run-Process 'reg' ('ADD "HKCU\Software\Microsoft\Windows\CurrentVersion\RunOnce" /v 02RandomBackgroundColor /t REG_SZ /d "{0}" /f' -f $runOnceRandomBackgroundColor) -doNotRedirOutput $true -login $login -domain $domain -password $password -returnExitCode $false
    #Run-Process 'reg' ('ADD "HKCU\Software\Microsoft\Windows\CurrentVersion\RunOnce" /v 03DeleteTranscodedWallpaper /t REG_SZ /d "{0}" /f' -f $runOnceDeleteTranscodedWallpaper) -doNotRedirOutput $true -login $login -domain $domain -password $password -returnExitCode $false
    #$result = Run-Process 'reg' ('ADD "HKCU\Software\Microsoft\Windows\CurrentVersion\RunOnce" /v 04UpdatePerUserSystemParameters /t REG_SZ /d "{0}" /f' -f $runOnceUpdatePerUserSystemParameters) -doNotRedirOutput $true -login $login -domain $domain -password $password -returnExitCode $true

    if ((Is-ValidString $installedJob) -and (Test-Path $installedJob)) {

      $result = Run-Process 'reg' ('ADD "HKCU\Software\Microsoft\Windows\CurrentVersion\RunOnce" /v sevecek-run-once-user-env-update /t REG_SZ /d "{0}" /f' -f $installedJob) -doNotRedirOutput $true -login $login -domain $domain -password $password -returnExitCode $true
    }
  }

  return $result
}


function global:Update-PowershellLinks ([string[]] $types, [string] $envSevecekUserProfile)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $envSevecekUserProfile }

  DBG ('Update %sevecek_userprofile% variable: {0}' -f $envSevecekUserProfile)
  $env:sevecek_userprofile = $envSevecekUserProfile
  $env:sevecek_defaultprofile = $envSevecekUserProfile

  foreach ($onePSLinkLocation in $global:powerShellLinkLocations.Keys) {

    if (Contains-Safe $types ($global:powerShellLinkLocations[$onePSLinkLocation][0])) {

      [string] $psLinkPath = [Environment]::ExpandEnvironmentVariables((Join-Path $onePSLinkLocation 'Windows PowerShell.lnk'))
      [bool] $psLinkExists = Test-Path -Literal $psLinkPath
      DBG ('The PowerShell link exists on the current system: {0} | {1} | {2}' -f $global:thisOSVersionNormal, $psLinkPath, $psLinkExists)

      #DBGIF ('PS link (non)existing: {0} | {1} | exists = {2}' -f $global:thisOSVersionNormal, $psLinkPath, $psLinkExists) {         
      #              ($psLinkExists -and ($global:powerShellLinkLocations[$onePSLinkLocation] -notcontains $global:thisOSVersionNormal)) -or
      #              ((-not $psLinkExists) -and ($global:powerShellLinkLocations[$onePSLinkLocation] -contains $global:thisOSVersionNormal)) }

      if ($psLinkExists) {
         
        $ourPsLinkPath = Join-Path $global:rootDir ('User-Env\Win{0}\Windows PowerShell.lnk' -f $global:thisOSVersionNormal)
        DBG ('Update the PS link with our own customized version: our = {0} | {1:s} | sys = {2} | {3:s}' -f $ourPsLinkPath, (Get-Item $ourPsLinkPath).LastWriteTime, $psLinkPath, (Get-Item $psLinkPath).LastWriteTime)
        
        # Note: on Windows 2008 the links get current date for some weird reason
        DBGIF ('System PS link is newer than ours: {0}' -f $psLinkPath) { ((Get-Item $ourPsLinkPath).LastWriteTime -lt (Get-Item $psLinkPath).LastWriteTime) -and ($global:thisOSVersionNumber -ne 6.0) }
        
        if (((Get-Item $ourPsLinkPath).LastWriteTime -gt (Get-Item $psLinkPath).LastWriteTime) -or ($global:thisOSVersionNumber -eq 6.0)) {

          DBGSTART
          Copy-Item -Path $ourPsLinkPath -Destination $psLinkPath -Force
          DBGER $MyInvocation.MyCommand.Name $error
          DBGEND
        }
      }
    }
  }

  DBG ('Remove the %sevecek_userprofile% environment variable again')
  DBGSTART
  Remove-Item env:\sevecek_userprofile
  Remove-Item env:\sevecek_defaultprofile
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
}


function global:Update-UserEnvironment (
    [bool] $checkSystemWideIndicator = $true, 
    [bool] $checkPerUserIndicator = $true, 
    [bool] $systemWideEnvBasics = $true, 
    [bool] $systemWideDesktopIni = $true, 
    [bool] $systemWideNoIEESC = $true, 
    [bool] $systemWideNoSMInitial = $true, 
    [bool] $systemWideResetIEPolicy = $true,
    [bool] $systemWideIeLinkOnDesktopIfNecessary = $true,
    [bool] $systemWideNoServerManager = $true,
    [bool] $systemWidePSLinks = $true,
    [bool] $perUserEnvBasics = $true, 
    [bool] $perUserDesktopIni = $true,
    [bool] $perUserNoSMInitial = $true,
    [bool] $perUserResetIEPolicy = $true,
    [bool] $perUserPSLinks = $true,
    [string] $alternativeRegPath
    )
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  
  [bool] $triggerGPUPDATE = $false

  if ($global:thisOSVersionNumber -lt 6.1) {

    $userEnvBasicsReg = 'user-env-basics-generic-5x.reg'

  } elseif ($global:thisOSVersionNumber -lt 10) {

    $userEnvBasicsReg = 'user-env-basics-generic-6x.reg'

  } else {

    $userEnvBasicsReg = 'user-env-basics-generic-10.reg'
  }

  if (Is-EmptyString $alternativeRegPath) {

    $userEnvBasicsReg = Join-Path (Join-Path $global:rootDir User-Env) $userEnvBasicsReg

  } else {

    $userEnvBasicsReg = Join-Path $alternativeRegPath $userEnvBasicsReg
  }

  DBG ('REG file path determined as: {0}' -f $userEnvBasicsReg)
  DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path $userEnvBasicsReg) }

  $userEnvUpdaterVersion = 7


  #=================
  $envIndicatorSys = Get-DataFileApp ('system-env-basics-v{0}' -f $userEnvUpdaterVersion) $null '.done'
  DBG ('Testing if SYSTEM environment has not been updated yet: {0} | {1} | check = {2}' -f $envIndicatorSys, (Test-Path $envIndicatorSys), $checkSystemWideIndicator)
  $shouldProceedWithSystem = (-not (Test-Path $envIndicatorSys)) -or (-not $checkSystemWideIndicator)
  DBG ('Will proceed with SYSTEM settings: {0}' -f $shouldProceedWithSystem)
  DBGIF $MyInvocation.MyCommand.Name { $shouldProceedWithSystem -and (-not $global:memberOfAdministrators) }

  #=================
  if ($thisOSVersion -like '5.*') {
    
    DBG ('Determine default user profile path on Win 5.x')

    DBGSTART
    $defaultUserProfileName = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" -Name "DefaultUserProfile").DefaultUserProfile
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $defaultUserProfileName }

    DBGSTART
    $defaultUserProfileDir = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" -Name "ProfilesDirectory").ProfilesDirectory
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $defaultUserProfileDir }

    $defaultUserHiveDir = Join-Path $defaultUserProfileDir $defaultUserProfileName
    DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path $defaultUserHiveDir) }

  } else {
    
    DBG ('Determine default user profile path on Win 6.x')

    DBGSTART
    $defaultUserHiveDir = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" -Name "Default").Default
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $defaultUserHiveDir }
    DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path $defaultUserHiveDir) }
  }
    
  $defaultUserHive = Join-Path $defaultUserHiveDir 'NTUSER.DAT'
  DBG ('Default user profile path: {0} | {1}' -f $defaultUserHiveDir, $defaultUserHive)
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $defaultUserHive }
  DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path -Literal $defaultUserHive) }


  #=================
  DBG ('System wide environment basics: {0} | {1}' -f $systemWideEnvBasics, ($systemWideEnvBasics -and $shouldProceedWithSystem))

  if ($systemWideEnvBasics -and $shouldProceedWithSystem) {

    # Note: ScreenColors = 
    #                      0000000e YELLOW
    #                      0000000a GREEN
    #                      0000000b CYAN
    #                      0000000f WHITE

    DBG ('User environment into SYSTEM')
    $envRegFile = Get-DataFileApp 'user-env-basics-HKU' $null '.reg'
    $argReplacement = 'regHiveRoot${0}|cmdColor${1}' -f (Escape-ForMultiValue 'HKEY_USERS\.Default', '0000000a')
    Replace-ArgumentsInFile $userEnvBasicsReg $argReplacement $envRegFile Unicode
    Run-Process 'REGEDIT' ('/S "{0}"' -f $envRegFile)

    DBG ('User environment into NetworkService')
    $envRegFile = Get-DataFileApp 'user-env-basics-NTWSVC' $null '.reg'
    $argReplacement = 'regHiveRoot${0}|cmdColor${1}' -f (Escape-ForMultiValue 'HKEY_USERS\S-1-5-20', '0000000b')
    Replace-ArgumentsInFile $userEnvBasicsReg $argReplacement $envRegFile Unicode
    Run-Process 'REGEDIT' ('/S "{0}"' -f $envRegFile)

    DBG ('User environment into LocalService')
    $envRegFile = Get-DataFileApp 'user-env-basics-LOCSVC' $null '.reg'
    $argReplacement = 'regHiveRoot${0}|cmdColor${1}' -f (Escape-ForMultiValue 'HKEY_USERS\S-1-5-19', '0000000f')
    Replace-ArgumentsInFile $userEnvBasicsReg $argReplacement $envRegFile Unicode
    Run-Process 'REGEDIT' ('/S "{0}"' -f $envRegFile)

    DBG ('User environment into Default User')

    Mount-RegistryHive $defaultUserHive 'DefaultUserHive'

    $envRegFile = Get-DataFileApp 'user-env-basics-HKDefaultUser' $null '.reg'
    $argReplacement = 'regHiveRoot${0}|cmdColor${1}' -f (Escape-ForMultiValue 'HKEY_LOCAL_MACHINE\DefaultUserHive', '0000000e')
    Replace-ArgumentsInFile $userEnvBasicsReg $argReplacement $envRegFile Unicode
    Run-Process 'REGEDIT' ('/S "{0}"' -f $envRegFile)

    Dismount-RegistryHive 'DefaultUserHive'
  }


  #=================
  DBG ('System wide PowerShell links: {0}' -f $systemWidePSLinks, ($systemWidePSLinks -and $shouldProceedWithSystem))
  if ($systemWidePSLinks -and $shouldProceedWithSystem) {

    DBG ('Is this a suppported system: {0}' -f ($global:thisOSVersionNumber -ge 6.0))

    if ($global:thisOSVersionNumber -ge 6.0) {

      DBGIF ('Updating PowerShell links on a new system: {0}' -f $global:thisOSVersionNumber) { $global:thisOSVersionNumber -gt 10.0 }
      Update-PowershellLinks -types @('perSys') -envSevecekUserProfile $defaultUserHiveDir
    }
  }


  #=================
  DBG ('Remove DESKTOP.INI in PUBLIC/ALLUSERSPROFILE: {0} | {1}' -f $systemWideDesktopIni, ($systemWideDesktopIni -and $shouldProceedWithSystem))

  if ($systemWideDesktopIni -and $shouldProceedWithSystem) {

    if ($global:thisOSVersionNumber -ge 6.0) {

      if (Test-Path "$env:PUBLIC\Desktop\desktop.ini") {
       
        DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $env:PUBLIC }
        DBGSTART
        Remove-Item -Path "$env:PUBLIC\Desktop\desktop.ini" -Force -EA SilentlyContinue
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
      }
    }

    if (Test-Path "$env:ALLUSERSPROFILE\Desktop\desktop.ini") {
    
      DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $env:ALLUSERSPROFILE }
      DBGSTART
      Remove-Item -Path "$env:ALLUSERSPROFILE\Desktop\desktop.ini" -Force -EA SilentlyContinue
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
    }
  }


  #=================
  DBG ("Server manager without initial configuration: {0} | {1}" -f $systemWideNoSMInitial, ($systemWideNoSMInitial -and $shouldProceedWithSystem))

  if ($systemWideNoSMInitial -and $shouldProceedWithSystem) {

    if ($thisOSRole -notlike '*workstation*') {

      Set-RegistryValue 'HKLM:\SOFTWARE\Microsoft\ServerManager\Oobe' DoNotOpenInitialConfigurationTasksAtLogon 1 DWord

      if ($global:thisOSVersionNumber -ge 10) {
            
        # Note: disable the invitation to try managing servers with Windows Admin Center
        Set-RegistryValue 'HKLM:\SOFTWARE\Microsoft\ServerManager' DoNotPopWACConsoleAtSMLaunch 1 DWord
      }
    }

    # Note: allegedly, this value has any effect only if set together with the previous DoNotOpenInitialConfigurationTasksAtLogon
    #       I didn't take the time to test otherwise
    #==================
    DBG ('Disable server manager: {0} | {1}' -f $systemWideNoServerManager, ($systemWideNoServerManager -and $shouldProceedWithSystem))

    if ($systemWideNoServerManager -and $shouldProceedWithSystem) {

      if ($thisOSRole -notlike '*workstation*') {

        Set-RegistryValue 'HKLM:\SOFTWARE\Microsoft\ServerManager' DoNotOpenServerManagerAtLogon 1 DWord
      }
    }
  }


  #=================
  DBG ('Should we create IE link on all users desktop: {0} | {1}' -f $systemWideIeLinkOnDesktopIfNecessary, ($systemWideIeLinkOnDesktopIfNecessary -and $shouldProceedWithSystem))

  if ($systemWideIeLinkOnDesktopIfNecessary -and $shouldProceedWithSystem -and ($global:thisOSVersionNumber -ge 10)) {

    $ieLink = Join-Path $env:AllUsersProfile 'Desktop\Internet Explorer.lnk'
    DBG ('Create IE link on the all-users desktop: exists = {0} | {1}' -f (Test-Path -Literal $ieLink), $ieLink)
    
    if (-not (Test-Path -Literal $ieLink)) {

      $ieExe = Join-Path $env:ProgramFiles 'Internet Explorer\iexplore.exe'
      DBGIF ('Internet Explorer executable does not exist: {0}' -f $ieExe) { -not (Test-Path -Literal $ieExe) }

      if (Test-Path -Literal $ieExe) {

        DBG ('Create the IE link: {0}' -f $ieExe)
        DBGSTART
        $shell = New-Object -Com WScript.Shell
        $shortcut = $shell.CreateShortcut($ieLink)
        $shortcut.TargetPath = $ieExe
        $shortcut.Save()
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
      }
    }
  }


  #=================
  DBG ('Should we disable IE ESC: {0} | {1}' -f $systemWideNoIEESC, ($systemWideNoIEESC -and $shouldProceedWithSystem))
  
  if ($systemWideNoIEESC -and $shouldProceedWithSystem) {
      
    $activeSetup1 = 'HKLM:\Software\Microsoft\Active Setup\Installed Components\{A509B1A7-37EF-4b3f-8CFC-4F3A74704073}'
    $activeSetup2 = 'HKLM:\Software\Microsoft\Active Setup\Installed Components\{A509B1A8-37EF-4b3f-8CFC-4F3A74704073}'


    if ($global:thisOSRole -notlike '*workstation*') {

      DBGSTART
      DBG ('Some basic params to check first: {0} | {1} | {2}' -f 
                (Test-Path (Join-Path $env:SystemRoot System32\iesetup.dll)),
                (Get-ItemProperty $activeSetup1).ComponentID,
                (Get-ItemProperty $activeSetup1).IsInstalled,
                (Get-ItemProperty $activeSetup2).ComponentID,
                (Get-ItemProperty $activeSetup2).IsInstalled
                )

      [bool] $ieescValid = (Test-Path (Join-Path $env:SystemRoot System32\iesetup.dll)) -and 
                           ((Get-ItemProperty $activeSetup1).ComponentID -eq 'IEHardenAdmin') -and  # .IsInstalled -eq 1  on 6+ while 0 on 5.2
                           ((Get-ItemProperty $activeSetup2).ComponentID -eq 'IEHardenUser')        # .IsInstalled -eq 1  on 6+ while 0 on 5.2
      DBGEND # Note: just ignore the non-existing errors
                           

      DBG ('IE ESC installed and available to be configured: {0}' -f $ieescValid)
      DBGIF $MyInvocation.MyCommand.Name { -not $ieescValid }

      if ($ieescValid) {

        Set-RegistryValue $activeSetup1 'IsInstalled' 0 DWord
        Set-RegistryValue $activeSetup2 'IsInstalled' 0 DWord

        if ($global:thisOSVersionNumber -lt 6) {

          Set-RegistryValue 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Setup\OC Manager\Subcomponents' 'iehardenadmin' 0 DWord
          Set-RegistryValue 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Setup\OC Manager\Subcomponents' 'iehardenuser' 0 DWord

        } else {

          Run-Process 'Rundll32' 'iesetup.dll, IEHardenLMSettings'
        }

        Run-Process 'Rundll32' 'iesetup.dll, IEHardenUser'
        Run-Process 'Rundll32' 'iesetup.dll, IEHardenAdmin'
        Run-Process 'Rundll32' 'iesetup.dll, IEHardenMachineNow'

        <#
        Remove-Item -Path $activeSetup1 -Force -EV er -EA SilentlyContinue
        DBGER $MyInvocation.MyCommand.Name $er
        Remove-Item -Path $activeSetup2 -Force -EV er -EA SilentlyContinue
        DBGER $MyInvocation.MyCommand.Name $er
        #>
      }

    } else {

      DBG ('Do not disable IE ESC as we are running on a workstation')
    }
  }

  
  #==================
  DBG ('Should we reset system IE group policy settings if any found in registry manually set: {0} | {1}' -f $systemWideResetIEPolicy, ($systemWideResetIEPolicy -and $shouldProceedWithSystem))

  if ($systemWideResetIEPolicy -and $shouldProceedWithSystem) {

    if (Test-Path 'HKLM:\SOFTWARE\Policies\Microsoft\Internet Explorer') {

      DBG ('Remove the system IE policy settings')
      DBGSTART
      Remove-Item 'HKLM:\SOFTWARE\Policies\Microsoft\Internet Explorer' -Recurse -Force
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND

      $triggerGPUPDATE = $true
    }
  }


  #=================
  DBG ('Will save SYSTEM wide indicator: {0}' -f $checkSystemWideIndicator)

  if ($checkSystemWideIndicator) {

    DBG ("Saving system environment indicator to: {0}" -f $envIndicatorSys)
    New-Item -Path $envIndicatorSys -ItemType File -Force -EV er -EA SilentlyContinue | Out-Null
    DBGER $MyInvocation.MyCommand.Name $er
  }





  #=================
  DBG ('See if we proceed with USER environment settings')
  DBGSTART
  $userSID = [Security.Principal.WindowsIdentity]::GetCurrent($false).User.Value
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $userSID }
  DBGIF $MyInvocation.MyCommand.Name { $userSID -notmatch $global:rxSID }
 
  $envIndicator = Get-DataFileApp ('user-env-basics-v{0}-{1}' -f $userEnvUpdaterVersion, $userSID) $null '.done'
  #$envIndicator = Join-Path $env:TEMP "\$global:buildupRootFolder\user-env-basics.done"
  DBG ('Testing if USER environment has not been updated yet: {0} | {1} | check = {2}' -f $envIndicator, (Test-Path -Literal $envIndicator), $checkPerUserIndicator)
  $shouldProceedWithUser = (-not (Test-Path -Literal $envIndicator)) -or (-not $checkPerUserIndicator)
  DBG ('Will proceed with USER settings: {0}' -f $shouldProceedWithUser)


  #=================
  DBG ('Per user environment basics: {0} | {1}' -f $perUserEnvBasics, ($perUserEnvBasics -and $shouldProceedWithUser))

  if ($perUserEnvBasics -and $shouldProceedWithUser) {
  
    DBG ('User environment into HKCU')
    $envRegFile = Get-DataFileApp 'user-env-basics-HKCU' $null '.reg'
    $argReplacement = 'regHiveRoot${0}|cmdColor${1}' -f (Escape-ForMultiValue 'HKEY_CURRENT_USER', '0000000e')
    Replace-ArgumentsInFile $userEnvBasicsReg $argReplacement $envRegFile Unicode
    Run-Process 'REGEDIT' ('/S "{0}"' -f $envRegFile)

    Run-Process 'rundll32' 'user32.dll,UpdatePerUserSystemParameters'

    Define-Win32Api

    DBGSTART
    [Sevecek.Win32Api.User32]::SetSysColors(1, @(1), @(0,0,0))
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
  }


  #=================
  DBG ('Per user PowerShell links: {0}' -f $perUserPSLinks, ($perUserPSLinks -and $shouldProceedWithUser))
  if ($perUserPSLinks -and $shouldProceedWithUser) {

    DBG ('Is this a suppported system: {0}' -f ($global:thisOSVersionNumber -ge 6.0))

    if ($global:thisOSVersionNumber -ge 6.0) {

      DBGIF ('Updating PowerShell links on a new system: {0}' -f $global:thisOSVersionNumber) { $global:thisOSVersionNumber -gt 10.0 }
      Update-PowershellLinks -types @('perUsr') -envSevecekUserProfile $env:USERPROFILE
    }
  }



  #=================
  DBG ('Remove DESKTOP.INI in USERPROFILE: {0} | {1}' -f $perUserDesktopIni, ($perUserDesktopIni -and $shouldProceedWithUser))

  if ($perUserDesktopIni -and $shouldProceedWithUser) {

    $desktopINI = (Join-Path ([Environment]::GetFolderPath([Environment+SpecialFolder]::Desktop)) 'desktop.ini')

    if (Test-Path $desktopINI) {

      # Note: this is the correct way instead of using the %USERPROFILE variable
      DBGSTART
      Remove-Item -Path $desktopINI -Force
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
    }

    # Note: just to get rid of the file completelly if anything
    $desktopINI = "$env:USERPROFILE\Desktop\desktop.ini"

    if (Test-Path $desktopINI) {

      DBGSTART
      Remove-Item -Path $desktopINI -Force
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
    }
  }


  #==================
  DBG ("Server manager without initial configuration: {0} | {1}" -f $perUserNoSMInitial, ($perUserNoSMInitial -and $shouldProceedWithUser))

  if ($perUserNoSMInitial -and $shouldProceedWithUser) {

    if ($thisOSRole -notlike '*workstation*') {

      Set-RegistryValue 'HKCU:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\srvWiz' '(Default)' 0 DWord
      Set-RegistryValue 'HKCU:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Explorer\tips' Show 0 DWord
    }
  }


  #==================
  DBG ('Should we reset user IE group policy settings if any found in registry manually set: {0} | {1}' -f $perUserResetIEPolicy, ($perUserResetIEPolicy -and $shouldProceedWithUser))

  if ($perUserResetIEPolicy -and $shouldProceedWithUser) {

    if (Test-Path 'HKCU:\SOFTWARE\Policies\Microsoft\Internet Explorer') {

      DBG ('Remove the user IE policy settings')
      DBGSTART
      Remove-Item 'HKCU:\SOFTWARE\Policies\Microsoft\Internet Explorer' -Recurse -Force
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND

      $triggerGPUPDATE = $true
    }
  }



  #==================
  DBG ('Will save per USER indicator: {0}' -f $checkPerUserIndicator)

  if ($checkPerUserIndicator) {

    DBG ("Saving user environment indicator to: {0}" -f $envIndicator)
    New-Item -Path $envIndicator -ItemType File -Force -EV er -EA SilentlyContinue | Out-Null
    DBGER $MyInvocation.MyCommand.Name $er
  }


  #==================
  if ($triggerGPUPDATE) {

    DBG ('Force GPUPDATE to let rebuild any IE policy settings which really came from policy')
    Run-Process 'gpupdate' '/force'
  }
}


function global:Get-MachineFinishShareInfo ([string] $finishShareInfo)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  $finishInfo = New-Object PSObject

  [string[]] $finishInfoTokens = Split-MultiValue $finishShareInfo
  DBG ('Finish share info split: {0} | {1} | {2} | {3}' -f $finishInfoTokens[0], $finishInfoTokens[1], $finishInfoTokens[2], $finishInfoTokens[3])
  DBGIF $MyInvocation.MyCommand.Name { $finishInfoTokens.Length -ne 4 }

  Add-Member -Input $finishInfo -MemberType NoteProperty -Name server -Value $finishInfoTokens[0]
  Add-Member -Input $finishInfo -MemberType NoteProperty -Name share -Value $finishInfoTokens[1]
  Add-Member -Input $finishInfo -MemberType NoteProperty -Name login -Value $finishInfoTokens[2]
  Add-Member -Input $finishInfo -MemberType NoteProperty -Name pwd -Value $finishInfoTokens[3]

  Add-Member -Input $finishInfo -MemberType NoteProperty -Name path -Value ('{0}\{1}' -f $finishInfo.server, $finishInfo.share)

  return $finishInfo
}


function global:Prepare-FinalizationFile ([string] $share, [string] $hostName, [string] $domain, [string] $login, [string] $pwd)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $share }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $hostName }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $domain }

  Connect-NetworkCredentials -unc $share -fullLogin $login -pwd $pwd

  [string] $finalizationFile = Join-Path (Join-Path $share 'Sevecek VM Buildup') ('{0}.{1}.ini' -f $hostName, $domain)
  DBG ('Finalization file aready exists: {0} | {1}' -f (Test-Path -Literal $finalizationFile), $finalizationFile)

  if (-not (Test-Path -Literal $finalizationFile)) {

    DBG ('Create the finalization file and its path')
    [string] $finalizationFolder = Split-Path -Parent $finalizationFile
    DBG ('Finalization folder exists: {0} | {1}' -f (Test-Path -Literal $finalizationFolder), $finalizationFolder)

    if (-not (Test-Path -Literal $finalizationFolder)) {

      DBG ('Create the finalization folder path first: {0}' -f $finalizationFolder)
      DBGSTART
      New-Item -Path $finalizationFolder -ItemType Directory -Force | Out-Null
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
    }

    [hashtable] $finalizationValues = @{}
    $finalizationValues['sevecek-VMB-Hostname'] = $hostName
    $finalizationValues['sevecek-VMB-MachineFinished'] = $null
    $finalizationValues['sevecek-VMB-PhaseFinished'] = $null

    Save-IniFile -path $finalizationFile -values $finalizationValues
  }

  return $finalizationFile
}

function global:Set-KvpValue ([string] $name, [string] $value)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $name }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $value }

  if ((Is-ValidString $name) -and (Is-ValidString $value)) {

    $guestKvpRegKey = 'SOFTWARE\Microsoft\Virtual Machine\Guest'

    [string] $currentValueStored = Get-RegValue '.' $guestKvpRegKey $name String
    DBG ('Current KVP value: {0}' -f $currentValueStored)

    DBG ('Setting KVP value: {0} | {1}' -f $name, $value)
    Set-RegistryValue "HKLM:\$guestKvpRegKey" $name $value String
  }
}

function global:StartFinish-KvpPhase ([string] $state, [switch] $startPhase, [switch] $finishPhase, [switch] $finishMachine)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { -not $global:runningInHyperV }
  DBGIF $MyInvocation.MyCommand.Name { $global:vmICVersionNumber -lt 6.2 }

  if (($global:runningInHyperV) -and ($global:vmICVersionNumber -ge 6.2)) {

    DBGIF $MyInvocation.MyCommand.Name { -not ($startPhase -xor $finishPhase -xor $finishMachine) }

    $guestKvpRegKey = 'SOFTWARE\Microsoft\Virtual Machine\Guest'

    if ($startPhase) {

      $kvpValueName = 'sevecek-VMB-PhaseStarted'

    } elseif ($finishPhase) {

      $kvpValueName = 'sevecek-VMB-PhaseFinished'

    } elseif ($finishMachine) {

      $kvpValueName = 'sevecek-VMB-MachineFinished'
    }

    [string] $currentPhasesStored = Get-RegValue '.' $guestKvpRegKey $kvpValueName String
    DBG ('Current phases KVP value: {0}' -f $currentPhasesStored)

    if ($finishMachine) {

      DBG ('Finishing machine: {0}' -f $state)
      Set-RegistryValue "HKLM:\$guestKvpRegKey" $kvpValueName $state String

    } else {

      [string] $newPhasesToStore = Add-MultiValue $currentPhasesStored $state -unique $true
      DBG ('New phases KVP value: {0}' -f $newPhasesToStore)
      Set-RegistryValue "HKLM:\$guestKvpRegKey" $kvpValueName $newPhasesToStore String
    }
  }
}

function global:Finish-Machine ([bool] $unfinish, [string] $subPhase, [switch] $doNotAssertAlreadyFinished)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))


  if ($global:runningInHyperV -and ($global:vmICVersionNumber -ge 6.2)) {

    DBG ('Running on VM with HVIC 2012 or newer. Finish into registry as well in order to use the key-value-pair (KVP) Data Exchange integration component')

    $guestKvpRegKey = 'SOFTWARE\Microsoft\Virtual Machine\Guest'

    if (Is-EmptyString $subPhase) {

      DBG ('Finishing machine: {0}' -f (-not $unfinish))
      StartFinish-KvpPhase -state ([string] (-not $unfinish)) -finishMachine
      #Set-RegistryValue "HKLM:\$guestKvpRegKey" 'sevecek-VMB-MachineFinished' ([string] (-not $unfinish)) String

    } else {

      DBG ('Finishing phase: {0}' -f $subPhase)
      StartFinish-KvpPhase -state $subPhase -finishPhase
    }
  }


  if ($global:thisOSRole -notlike '*workgroup*') {
  
    DBG ("Getting joiner credentials first.")
    
    $joinerCred = Get-JoinerCredentials $vmConfig

    #
    #

    DBG ('Connect to AD')
    [System.Collections.ArrayList] $deList = @()
    $rootDSE = Get-DE "$global:thisComputerDomain/RootDSE" ([ref] $deList) $joinerCred.FullLogin $joinerCred.Pwd
    $adSvc = Get-OthDE ('CN=Sevecek VM Buildup,CN=Services,' + (GDES $rootDSE configurationNamingContext)) $rootDSE ([ref] $deList) -doNotAssertSuccess $true

    if (Is-NonNull $adSvc) {

      #DBG ("Creating AD object for VM: {0}.{1}" -f $global:thisComputerHost, $global:thisComputerDomain)

      #$dnPath = New-Object System.Collections.ArrayList
      #$dnPath.Add((New-DNComponent ('CN={0}.{1}' -f $global:thisComputerHost, $global:thisComputerDomain) 'container' @{ 'sevecek-VMB-MachineFinished' = $global:VM_READY }))

      #$vmDE = Create-DNPath $dnPath $adSvc ([ref] $deList) #('GA${0}' -f $joinerCred.FullLogin)

      DBGIF $MyInvocation.MyCommand.Name { $global:thisComputerHost -ne $vmConfig.hostName }
      DBGIF $MyInvocation.MyCommand.Name { $global:thisComputerDomain -ne (Get-MachineDomain $vmConfig) }

      $vmDE = Get-SubDE ('CN={0}.{1}' -f $global:thisComputerHost, $global:thisComputerDomain) $adSvc ([ref] $deList)

      if (Is-NonNull $vmDE) {

        DBG ("Setting FIN flag in VM object: {0}" -f $vmDE.Path)
    
        if (-not $unfinish) {

          if (Is-EmptyString $subPhase) {

            DBG ('Doing final closure')
            DBGIF $MyInvocation.MyCommand.Name { (GDES $vmDE 'sevecek-VMB-MachineFinished') -eq $global:VM_READY }

            DBGSTART
            $vmDE.Put('sevecek-VMB-MachineFinished', $global:VM_READY)
            $vmDE.SetInfo()
            DBGER $MyInvocation.MyCommand.Name $error
            DBGEND

          } else {

            DBG ('Finalizing a phase: {0}' -f $subPhase)
            
            [bool] $alreadyFinishedSubphase = Has-MultiValue (GDES $vmDE 'sevecek-VMB-PhaseFinished') $subPhase
            DBG ('Subphase already finished: {0}' -f $alreadyFinishedSubphase)
            DBGIF $MyInvocation.MyCommand.Name { (-not $doNotAssertAlreadyFinished) -and $alreadyFinishedSubphase }

            if (-not $alreadyFinishedSubphase) {
                        
              DBGSTART
              $vmDE.PutEx($global:PutExAdd, 'sevecek-VMB-PhaseFinished', @($subPhase))
              $vmDE.SetInfo()
              DBGER $MyInvocation.MyCommand.Name $error
              DBGEND
            }
          }

        } else {

          DBGSTART
          $vmDE.PutEx($global:PutExClear, 'sevecek-VMB-MachineFinished', $null)
          $vmDE.PutEx($global:PutExClear, 'sevecek-VMB-PhaseFinished', $null)
          $vmDE.SetInfo()
          DBGER $MyInvocation.MyCommand.Name $error
          DBGEND
        }
      }
    }

    Dispose-List ([ref] $deList)

    #
    #

    DBG ('Should we finalize into file share: {0}' -f (Is-ValidString $vmConfig.finalizationShare))
    if (Is-ValidString $vmConfig.finalizationShare) {

      [string] $finalizationFile = Prepare-FinalizationFile -share $vmConfig.finalizationShare -hostName $global:thisComputerHost -domain $global:thisComputerDomain -login $joinerCred.FullLogin -pwd $joinerCred.Pwd
      [hashtable] $finalizationValues = Read-IniFile -path $finalizationFile

      if (-not $unfinish) {

        if (Is-EmptyString $subPhase) {

          DBG ('Doing final closure')
          DBGIF $MyInvocation.MyCommand.Name { $finalizationValues['sevecek-VMB-MachineFinished'] -eq $global:VM_READY }
          $finalizationValues['sevecek-VMB-MachineFinished'] = $global:VM_READY

        } else {

          DBG ('Finalizing a phase: {0}' -f $subPhase)
          DBGIF $MyInvocation.MyCommand.Name { Has-MultiValue ($finalizationValues['sevecek-VMB-PhaseFinished']) $subPhase }
          $finalizationValues['sevecek-VMB-PhaseFinished'] = Add-MultiValue -multivalue ($finalizationValues['sevecek-VMB-PhaseFinished']) -newValues @($subPhase) -unique $true
        }

      } else {

        DBG ('Cleaning the state')
        $finalizationValues['sevecek-VMB-MachineFinished'] = $null
        $finalizationValues['sevecek-VMB-PhaseFinished'] = $null
      }

      Save-IniFile -path $finalizationFile -value $finalizationValues
    }
  }
}


function global:Wait-Machine ([string] $waitParamsStr, [switch] $doNotRandomWait, [PSCustomObject] $waiterCredentials, [string] $subPhase)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $waitParamsStr }

  [System.Collections.ArrayList] $deList = @()

  $waitParams = Split-MultiValue $waitParamsStr
    
  $waitForVM = $waitParams[0]
  $waitDomain = $waitParams[1]
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $waitForVM }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $waitDomain }

  DBG ('Wait until machine is available: {0} | {1}' -f $waitForVM, $waitDomain)

  if (($waitForVM -eq $global:thisComputerHost) -and ($global:thisComputerDomain -eq $waitDomain)) {

    # Cases such as SQL server on the same machine as SharePoint etc.
    DBG ('Wait finished immediately, we do not wait for ourselves.')

  } else {

    if (Is-NonNull $waiterCredentials) {

      DBG ('Will use the explicitly supplied joiner credentials: {0} | {1} | {2}' -f $waiterCredentials.joinerLogin, $waiterCredentials.joinerDomain, $waiterCredentials.joinerFullLogin)
      $joinerCred = $waiterCredentials

    } else {

      DBG ('Joiner credentials not supplied, going to determine some default set')
      $joinerCred = Get-JoinerCredentials $vmConfig
    }

    #----------------
    DBG ("Testing domain access (ANONYMOUS): {0}" -f $waitDomain)
    
    do {
  
      Run-Process 'IPCONFIG' '/flushdns'
      Run-Process 'IPCONFIG' '/renew'
  
      $ad = Get-DE "$waitDomain/RootDSE" ([ref] $deList) '-' $null $null -doNotAssertSuccess $true
    
      [string] $rootDSEStatus = GDES $ad isSynchronized
    
      if ((Is-ValidString($rootDSEStatus)) -and (Parse-BoolSafe($rootDSEStatus))) {
    
        DBG ("AD responded OK: {0}, {1} (version F:{2}/D:{3}/DC:{4})" -f (GDES $ad dNSHostName), (GDES $ad defaultNamingContext), (GDES $ad forestFunctionality), (GDES $ad domainFunctionality), (GDES $ad domainControllerFunctionality))
        break
      }
    
      else {
    
        DBG ("AD '{0}' not responding. Sleeping for another 17sec." -f $waitDomain)
        Start-Sleep 17
      }

      # Should dispose the list as soon as possible, bue to either maximum connection limit in case 
      # connection pooling is used (I do not know exactly if this is the case)
      # or in case there is some memory leak/limit on number of concurrently active DirectoryEntry instances
      Dispose-List ([ref] $deList)

    } while ($true)

    #-------------------
    DBG ("Waiting for machine to finish (AUTHENTICATED): {0}, user = {1} @ {2}" -f $waitForVM, $joinerCred.Login, $joinerCred.Domain)
  
    do {

      [string] $vmStatus = $null

      #
      #

      DBG ('Could we use finalization into from file share: {0}' -f (Is-ValidString $vmConfig.finalizationShare))
      if (Is-ValidString $vmConfig.finalizationShare) {

        Run-Process 'IPCONFIG' '/flushdns'
        Run-Process 'IPCONFIG' '/renew'
  
        [string] $finalizationFile = Join-Path (Join-Path $vmConfig.finalizationShare 'Sevecek VM Buildup') ('{0}.{1}.ini' -f $waitForVM, $waitDomain)
        Connect-NetworkCredentials -unc $finalizationFile -fullLogin $joinerCred.FullLogin -pwd $joinerCred.Pwd
        
        if (Test-Path -Literal $finalizationFile) {

          [hashtable] $finalizationValues = Read-IniFile -path $finalizationFile
      
          if (Is-ValidString $subPhase) {

            $vmStatus = $finalizationValues['sevecek-VMB-PhaseFinished']

          } else {

            $vmStatus = $finalizationValues['sevecek-VMB-MachineFinished']
          }

          DBG ('Machine status obtained from file: {0}' -f $vmStatus)
        }

      } else {

        # Note: we must get RootDSE here again as the previous check might have succeeded shortly before
        #       the DC went to restart
        # Note: in order to authenticate against rootDSE, the path must be domain/RootDSE. When using just LDAP://RootDSE, the 
        #       authentication does not work on Windows 2012

        DBG ('Get rootDSE: {0}, user = {1}' -f $waitDomain, $joinerCred.FullLogin)
        $rootDSE = Get-DE "$waitDomain/RootDSE" ([ref] $deList) $joinerCred.FullLogin $joinerCred.Pwd $null -doNotAssertSuccess $true
    
        if (Is-NonNull $rootDSE) {

          DBG ('Get machine status: {0}' -f $waitForVM)
          $adCfg = Get-OthDE (GDES $rootDSE configurationNamingContext) $rootDSE ([ref] $deList) -doNotAssertSuccess $true

          if (Is-NonNull $adCfg) {

            $vm = Get-SubDE "CN=$waitForVM.$waitDomain,CN=Sevecek VM Buildup,CN=Services" $adCfg ([ref] $deList) -doNotAssertSuccess $true
       
            if (Is-NonNull $vm) {

              if (Is-ValidString $subPhase) {

                $vmStatus = GDES $vm 'sevecek-VMB-PhaseFinished'

              } else {

                $vmStatus = GDES $vm 'sevecek-VMB-MachineFinished'
              }
            }
          }

          DBG ('Machine status obtained from AD: {0}' -f $vmStatus)
        }
      }

      # Should dispose the list as soon as possible, bue to either maximum connection limit in case 
      # connection pooling is used (I do not know exactly if this is the case)
      # or in case there is some memory leak/limit on number of concurrently active DirectoryEntry instances
      Dispose-List ([ref] $deList)

      #
      #

      [bool] $machineReady = $false

      if (Is-ValidString $vmStatus) {
        
        if (Is-ValidString $subPhase) {

          DBG ('Looking for phase terminator: {0}' -f $subPhase)
          $machineReady = Has-MultiValue $vmStatus $subPhase

        } else {

          $machineReady = $vmStatus -eq $global:VM_READY
        }
      }
    
    
      if ($machineReady) {
        
        DBG ("Machine {0}.{1} ready, going to proceed..." -f $waitForVM, $waitDomain)
        break
      }
      
      else {
      
        DBG ("Machine {0}.{1} not ready yet, waiting for another 17 sec." -f $waitForVM, $waitDomain)
        Start-Sleep 17
      }

      
    } while ($true)

  
    # Just to be on the safe side
    Dispose-List ([ref] $deList)

    $interBuildWait = [int] $xmlConfig.vmBuilder.wait
    
    if (($interBuildWait -gt 3) -and (-not $doNotRandomWait)) {

      DBG ('HOST vmBuilder implemented wait delay between machines.')

      <# Seeding is reportadly not necessary according to documentation

      $sha = New-Object System.Security.Cryptography.SHA1CryptoServiceProvider
      $hashBytes = $sha.ComputeHash([System.Text.ASCIIEncoding]::ASCII.GetBytes(('{0}{1:s}' -f $global:thisComputerHost, (Get-Date))))
      $hexRandom = '{0:X2}{1:X2}{2:X2}{3:X2}' -f $hashBytes[0], $hashBytes[1], $hashBytes[2], $hashBytes[3]

      DBGSTART
      $randomSeed = [Int32]::Parse($hexRandom, [System.Globalization.NumberStyles]::HexNumber) # four bytes into Int32 generate even negative number ok
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND

      $interWait = [int] (((Get-Random -Maximum 60 -SetSeed $randomSeed) + 1) * $interBuildWait * 1.4) # solichaci konstanta 1.4

      #>

      $interWait = [int] (((Get-Random -Maximum 60) + 1) * $interBuildWait * 4) # solichaci konstanta 2.3

      DBG ('Going to insert random wait again for: {0:N2} [mins]' -f ($interWait / 60))
      Start-Sleep -Seconds ($interWait)
    }
  }
}


function global:Wait-MachineAllPrevious ([string[]] $allPreviousAppHosts)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBG ('Must wait for all the previous farm servers to complete one by one: {0} | #{1} | {2}' -f ((Get-CountSafe $allPreviousAppHosts) -gt 0), (Get-CountSafe $allPreviousAppHosts), ($allPreviousAppHosts -join ','))
  DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $allPreviousAppHosts) -lt 1 }

  if ((Get-CountSafe $allPreviousAppHosts) -gt 0) {

    foreach ($onePreviousAppHost in $allPreviousAppHosts) {

      Wait-Machine $onePreviousAppHost
    }
  }
}



function global:Get-MachineFromElement ([System.Xml.XmlElement] $machineOrSubNode)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $machineOrSubNode }

  [object] $res = $null

  if (Is-NonNull $machineOrSubNode) {

    $oneNode = $machineOrSubNode
    while ((Is-NonNull $oneNode) -and (Is-EmptyString $oneNode.hostName)) {
   
      $oneNode = $oneNode.psbase.ParentNode
    }

    DBGIF $MyInvocation.MyCommand.Name { Is-Null $oneNode }
    if ((Is-ValidString $oneNode.hostName) -and ($oneNode.name -ceq 'MACHINE')) {

      $res = $oneNode
    }
  }

  #DBG ('Get-MachineFromElement result: {0}' -f ($res | Out-String))
  DBG ('Found machine: {0}' -f $res.hostName)

  return $res
}


function global:Get-SvcFromElement ([System.Xml.XmlElement] $svcOrSubNode)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $svcOrSubNode }
  DBGIF $MyInvocation.MyCommand.Name { $svcOrSubNode.psbase.Name -ceq 'MACHINE' }

  [System.Xml.XmlElement] $res = $null

  if (Is-NonNull $svcOrSubNode) {

    $oneNode = $svcOrSubNode
    while ((Is-NonNull $oneNode) -and ($oneNode.psbase.ParentNode.psbase.Name -cne 'MACHINE')) {
    
      $oneNode = $oneNode.psbase.ParentNode
    }

    DBGIF $MyInvocation.MyCommand.Name { Is-Null $oneNode }
    DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $oneNode.psbase.ParentNode.hostName }
    $res = $oneNode
  }

  DBG ('Found service: host = {0} | svc = {1} | instance = {2}' -f $oneNode.psbase.ParentNode.hostName, $res.psbase.Name, $res.instance)

  return $res
}


function global:Get-MachineDomain ([System.Xml.XmlElement] $machineOrSubNode)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $machineOrSubNode }

  [string] $vmDomain = $null

  if (Is-NonNull $machineOrSubNode) {

    $oneNode = Get-MachineFromElement $machineOrSubNode
  
    if (Is-ValidString $oneNode.domain.new) {

      if (Is-EmptyString $oneNode.domain.join) {

        $vmDomain = $oneNode.domain.new
      
      } elseif ($oneNode.domain.new -like '*.*') {

        $vmDomain = $oneNode.domain.new

      } else {

        $vmDomain = '{0}.{1}' -f $oneNode.domain.new, $oneNode.domain.join
      }

    } elseif (Is-ValidString $oneNode.domain.join) {
  
      $vmDomain = $oneNode.domain.join
    }
  }

  DBG ('Machine domain: {0}' -f $vmDomain)

  return $vmDomain
}


function global:Get-MachineFQDN ([System.Xml.XmlElement] $machineOrSubNode, [switch] $doNotAssertFQDN)
# Mind the fact, FQDN may differ with dnsSuffix from Get-MachineDomain
# note that some code uses the FQDN, while others (such as Wait-Machine and Finish-Machine)
# use hostName.Get-MachineDomain instead
{
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $machineOrSubNode }
  #DBG ('Determine VM FQDN: {0}' -f ($machineOrSubNode | Out-String))

  [string] $res = ''

  if (Is-NonNull $machineOrSubNode) {

    $oneNode = Get-MachineFromElement $machineOrSubNode
    $hostName = $oneNode.hostName
  
    $vmDomain = Get-MachineDomain $machineOrSubNode

    if (Is-ValidString $vmDomain) {

      $dnsSuffix = $vmDomain
    }


    if (Is-ValidString $oneNode.domain.dnsSuffix) {
  
      $dnsSuffix = $oneNode.domain.dnsSuffix
    }
  
    $res = '{0}.{1}' -f $hostName, $dnsSuffix
  }

  DBG ('Get-MachineFQDN result: {0}' -f $res)
  DBGIF ('Machine FQDN not fully qualified: {0}' -f $res) { (-not $doNotAssertFQDN) -and ($res.Trim('.') -notlike '*.*.*') }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $res }

  return $res
}


function global:Get-MachineWaitParams ([System.Xml.XmlElement] $machineOrSubNode)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $machineOrSubNode }
  DBG ('Determine VM wait params: {0}' -f ($machineOrSubNode | Out-String))

  [string] $res = ''

  if (Is-NonNull $machineOrSubNode) {

    $oneNode = Get-MachineFromElement $machineOrSubNode
    $hostName = $oneNode.hostName

    $vmDomain = Get-MachineDomain $machineOrSubNode

    if (Is-ValidString $vmDomain) {
  
      $dnsSuffix = $vmDomain
    }
  
    if (Is-ValidString $oneNode.domain.dnsSuffix) {
  
      $dnsSuffix = $oneNode.domain.dnsSuffix
    }
  
    [string[]] $resArray = @($hostName, $dnsSuffix)
    $res = Format-MultiValue $resArray
  }

  DBG ('Get-MachineWaitParams result: {0}' -f $res)
  return $res
}


function global:Get-MachineSAMLogin ([System.Xml.XmlElement] $machineOrSubNode)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $machineOrSubNode }
  DBG ('Determine VM SAM login if a domain member: {0}' -f ($machineOrSubNode | Out-String))

  [string] $samLogin = ''

  if (Is-NonNull $machineOrSubNode) {

    $oneNode = Get-MachineFromElement $machineOrSubNode
    $machineSAMName = $oneNode.hostName

    if ($machineSAMName.Length -gt 15) { $machineSAMName = $machineSAMName.SubString(0, 15) }
  
    $machineDomain = Get-MachineDomain $machineOrSubNode

    if (Is-ValidString $machineDomain) {
  
      $samLogin = Get-SAMLogin ('{0}$' -f $machineSAMName) $machineDomain
    }
  
    DBGIF ('Cannot find machine SAM login: name = {0} | domain = {1}' -f $machineSAMName, $machineDomain) { Is-EmptyString $samLogin }
  }

  DBG ('Get-MachineSAMLogin result: {0}' -f $samLogin)
  return $samLogin
}


function global:Get-ServiceOrMachineDnsFQDNs ([System.Xml.XmlElement] $machineOrSubnode, [bool] $assertSingleName)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { Is-Null $machineOrSubnode }

  [Collections.ArrayList] $names = @()

  if (Is-NonNull $machineOrSubnode) {

    $foundDNSRRs = $machineOrSubnode.SelectNodes('.//dnsrr')
    $foundDNSAttrs = $machineOrSubnode.SelectNodes('.//*[@dns]')

    foreach ($oneFoundDNSRR in $foundDNSRRs) {

      DBG ('One DNS RR: {0} | {1} | {2}' -f $oneFoundDNSRR.type, $oneFoundDNSRR.rec, $oneFoundDNSRR.zone)

      if (($oneFoundDNSRR.type -eq 'A') -or ($oneFoundDNSRR.type -eq 'C') -or ($oneFoundDNSRR.type -eq 'CNAME')) {

        DBGIF $MyInvocation.MyCommand.Name { Is-EmptySTring $oneFoundDNSRR.rec }
        DBGIF $MyInvocation.MyCommand.Name { Is-EmptySTring $oneFoundDNSRR.zone }
        [string] $oneFQDN = ('{0}.{1}' -f $oneFoundDNSRR.rec.Trim('.'), (Strip-ValueFlags $oneFoundDNSRR.zone).Trim('.')).TrimStart('.')
        DBG ('One RR FQDN established: {0}' -f $oneFQDN)
        DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $oneFQDN }
        DBGIF ('Duplicate FQDN found: {0}' -f $oneFQDN) { Contains-Safe $names $oneFQDN }

        [void] $names.Add($oneFQDN)
      }
    }

    foreach ($oneFoundDNSAttr in $foundDNSAttrs) {

      DBG ('One legacy DNS attribute RR: {0}' -f $oneFoundDNSAttr.dns)
      [string] $oneFQDN = Strip-ValueFlags $oneFoundDNSAttr.dns
      DBG ('One legacy DNS FQDN established: {0}' -f $oneFQDN)
      DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $oneFQDN }
      DBGIF ('Duplicate FQDN found: {0}' -f $oneFQDN) { Contains-Safe $names $oneFQDN }
     
      [void] $names.Add($oneFQDN)
    }
  }

  DBG ('Returning found FQDNs: #{0} | {1}' -f $names.Count, ($names -join ','))
  DBGIF ('Multiple FQDN names found where only a single one is required: {0}' -f ($names -join ',')) { ($assertSingleName) -and ($names.Count -gt 1) }

  return ,$names.ToArray()
}


function global:Resolve-ClientFsPath ([System.Xml.XmlElement] $appFsNode)
{
  DBG ('Resolve FS path from client''s perspective: {0}, {1}, {2}, {3}' -f $appFsNode.appTag, $appFsNode.instance, $appFsNode.dir, $appFsNode.share)
  
  [string] $res = ''
  
  if ($appFsNode.instance -eq 'local') {

    if (Is-ValidString $appFsNode.share) {

      DBG ('Local FS location, shared path')
      $res = '\\{0}\{1}' -f (Get-MachineFQDN $appFsNode), $appFsNode.share

    } else {

      DBG ('Local FS location, local path')
      $res = Resolve-VolumePath $appFsNode.dir
    }
  }
  
  else {
  
    DBG ('Remote FS share location.')

    $fsServerFQDN = Get-FirstAppHostInInstance 'fs' $appFsNode.instance 'fqdn'
    DBG ('Found FS server node: {0}' -f $fsServerFQDN)

    $res = '\\{0}\{1}' -f $fsServerFQDN, $appFsNode.share
  }
  
  DBG ('FS path resolved to: {0}' -f $res)
  return $res
}


function global:Do-SubPhase ([string] $subPhase)
{
  $subPhasesFinished = $global:phaseCfg.sevecekBuildup.phaseId.subPhasesFinished
  DBG ('Currently finished subphases: #{0} : {1}' -f (Count-MultiValue $subPhasesFinished), $subPhasesFinished)

  $willDo = -not (Has-MultiValue $subPhasesFinished $subPhase)

  DBG ('Will do subphase: {0} | {1}' -f $subPhase, $willDo)

  return $willDo
}


function global:Finish-SubPhase ([string] $subPhase)
{
  $subPhasesFinished = $global:phaseCfg.sevecekBuildup.phaseId.subPhasesFinished
  DBG ('Finishing a new subphase: {0} | already = #{1} : {2}' -f $subPhase, (Count-MultiValue $subPhasesFinished), $subPhasesFinished)

  $global:phaseCfg.sevecekBuildup.phaseId.subPhasesFinished = [string] (Add-MultiValue $subPhasesFinished $subPhase $true)
  DBG ('Subphases finished now: {0}' -f $global:phaseCfg.sevecekBuildup.phaseId.subPhasesFinished)
}


function global:Publish-FSinIIS ([string] $path, [string] $iisApp, [string] $hostHeader, [string] $customSitePath, [string] $iisExtensions, [bool] $iisDoubleEscaping, [bool] $iisHighBit, [int] $iisUploadSize, [string] $iisMaxAge)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { (Is-EmptyString $path) }
  DBGIF $MyInvocation.MyCommand.Name { (-not (Test-Path $path)) }
  DBGIF $MyInvocation.MyCommand.Name { (Is-EmptyString $iisApp) }

  if ((Is-ValidString $path) -and ($iisApp)) {

    if ($global:thisOSVersion -like '5.*') {
    
      DBGIF 'Win5.0 IIS publishing for FS not yet implemented' { $global:thisOSVersion -like '5.*' }
    }


    $customWebSite = (Is-ValidString $hostHeader) -and (Is-ValidString $customSitePath)


    if ($global:thisOSVersionNumber -ge 6) {


      if ($customWebSite) {

        $fsSiteName = $hostHeader
        $appPoolName = '{0} AppPool' -f $hostHeader

        Create-IISSite $fsSiteName $customSitePath $null ('http/*:80:"{0}"' -f $hostHeader) $appPoolName $false $false $false $false

        Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}" /section:anonymousAuthentication /enabled:false /commit:apphost' -f $fsSiteName)
        Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}" /section:windowsAuthentication /enabled:false /commit:apphost' -f $fsSiteName)
        Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}" /section:basicAuthentication /enabled:false /commit:apphost' -f $fsSiteName)
        Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}" /section:digestAuthentication /enabled:false /commit:apphost' -f $fsSiteName)
        Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}" /section:clientCertificateMappingAuthentication /enabled:false /commit:apphost' -f $fsSiteName)
        Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}" /section:iisClientCertificateMappingAuthentication /enabled:false /commit:apphost' -f $fsSiteName)

        Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}" /section:defaultDocument /enabled:false /commit:apphost' -f $fsSiteName)
        Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}" /section:directoryBrowse /enabled:false /commit:apphost' -f $fsSiteName)
        Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}" /section:access /sslFlags:None /commit:apphost' -f $fsSiteName)
    
        Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}" /section:system.webServer/security/requestFiltering /allowDoubleEscaping:false' -f $fsSiteName)
        Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}" /section:system.webServer/security/requestFiltering /allowHighBitCharacters:false' -f $fsSiteName)
        Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}" /section:system.webServer/security/requestFiltering /fileExtensions.allowUnlisted:false' -f $fsSiteName)
        Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}" /section:system.webServer/security/requestFiltering /verbs.allowUnlisted:false' -f $fsSiteName)
        Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}" /section:system.webServer/security/requestFiltering /requestLimits.maxUrl:4096 /requestLimits.maxQueryString:0 /requestLimits.maxAllowedContentLength:0' -f $fsSiteName)

        if ($global:thisOSVersionNumber -ge 6.1) {

          Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}" /section:system.webServer/security/requestFiltering /~"verbs"' -f $fsSiteName)
          Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}" /section:system.webServer/security/requestFiltering /~"filteringRules"' -f $fsSiteName)
          Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}" /section:system.webServer/security/requestFiltering /~"denyUrlSequences"' -f $fsSiteName)
          Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}" /section:system.webServer/security/requestFiltering /~"denyQueryStringSequences"' -f $fsSiteName)
        
        } else {

          DBGIF 'AppCmd on Windows 2008 RTM does not support  using the ~ operator (verbs, filteringRules, denyUrlSequences, denyQueryStringSequences' { $true }
        }

      } else {

        $fsSiteName = 'Default Web Site'
        $appPoolName = 'FS Publishing AppPool'

        Create-IISAppPool $appPoolName
  
        DBG ('Host header binding requested for Default Web Site: {0} | {1}' -f (Is-ValidString $hostHeader), $hostHeader)
        if (Is-ValidString $hostHeader) {

          Run-Process $env:WINDIR\system32\inetsrv\appcmd ('set site /site.name:"{0}" /+bindings.[protocol=''http'',bindingInformation=''*:80:{1}'']' -f $fsSiteName, $hostHeader)
        }
      }


      if ($global:thisOSVersionNumber -eq 6) {

        Apply-NtfsDacl $path 'R$NT AUTHORITY\Network Service' $true

      } else {

        #Apply-NtfsDacl $path ('R$NT AUTHORITY\IUSR|R$IIS APPPool\{0}' -f $appPoolName) $true
        Apply-NtfsDacl $path ('R$IIS APPPool\{0}' -f $appPoolName) $true
      }


      Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('add app /site.name:"{0}" /path:"/{1}" /physicalPath:"{2}" /applicationPool:"{3}"' -f $fsSiteName, $iisApp, $path, $appPoolName)

      Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}/{1}" /section:anonymousAuthentication /enabled:true /commit:apphost' -f $fsSiteName, $iisApp)
      Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}/{1}" /section:anonymousAuthentication /userName:"" --password /commit:apphost' -f $fsSiteName, $iisApp)

      Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}/{1}" /section:windowsAuthentication /enabled:false /commit:apphost' -f $fsSiteName, $iisApp)
      Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}/{1}" /section:basicAuthentication /enabled:false /commit:apphost' -f $fsSiteName, $iisApp)
      Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}/{1}" /section:digestAuthentication /enabled:false /commit:apphost' -f $fsSiteName, $iisApp)
      Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}/{1}" /section:clientCertificateMappingAuthentication /enabled:false /commit:apphost' -f $fsSiteName, $iisApp)
      Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}/{1}" /section:iisClientCertificateMappingAuthentication /enabled:false /commit:apphost' -f $fsSiteName, $iisApp)
      
      Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}/{1}" /section:defaultDocument /enabled:false /commit:apphost' -f $fsSiteName, $iisApp)
      Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}/{1}" /section:directoryBrowse /enabled:false /commit:apphost' -f $fsSiteName, $iisApp)

      # to require SSL instead, type Ssl, may require SslNegotiateCert
      Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}/{1}" /section:access /sslFlags:None /commit:apphost' -f $fsSiteName, $iisApp)
    
      Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}/{1}" /section:system.webServer/security/requestFiltering /allowDoubleEscaping:{2}' -f $fsSiteName, $iisApp, $iisDoubleEscaping)
      
      Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}/{1}" /section:system.webServer/security/requestFiltering /allowHighBitCharacters:{2}' -f $fsSiteName, $iisApp, $iisHighBit)
      
      if (Is-EmptyString $iisExtensions) {

        Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}/{1}" /section:system.webServer/security/requestFiltering /fileExtensions.allowUnlisted:true' -f $fsSiteName, $iisApp)
      
      } else {

        Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}/{1}" /section:system.webServer/security/requestFiltering /fileExtensions.allowUnlisted:false' -f $fsSiteName, $iisApp)
 
        foreach ($oneExtension in (Split-MultiValue $iisExtensions)) {

          Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}/{1}" /section:system.webServer/security/requestFiltering /+"fileExtensions.[fileExtension=''{2}'',allowed=''true'']"' -f $fsSiteName, $iisApp, $oneExtension)
        }
      }

      if (Is-ValidString $iisMaxAge) {

        Run-Process $env:WINDIR\System32\inetsrv\appcmd.exe ('set config "{0}/{1}" /section:staticContent /clientCache.cacheControlMode:UseMaxAge /clientCache.cacheControlMaxAge:{2}' -f $fsSiteName, $iisApp, $iisMaxAge)
      }

      Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}/{1}" /section:system.webServer/security/requestFiltering /verbs.allowUnlisted:false' -f $fsSiteName, $iisApp)
      Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}/{1}" /section:system.webServer/security/requestFiltering /requestLimits.maxUrl:256 /requestLimits.maxQueryString:0 /requestLimits.maxAllowedContentLength:{2}' -f $fsSiteName, $iisApp, $iisUploadSize)

      if ($global:thisOSVersionNumber -ge 6.1) {

        Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}/{1}" /section:system.webServer/security/requestFiltering /~"verbs"' -f $fsSiteName, $iisApp)
        Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}/{1}" /section:system.webServer/security/requestFiltering /~"filteringRules"' -f $fsSiteName, $iisApp)
        Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}/{1}" /section:system.webServer/security/requestFiltering /~"denyUrlSequences"' -f $fsSiteName, $iisApp)
        Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}/{1}" /section:system.webServer/security/requestFiltering /~"denyQueryStringSequences"' -f $fsSiteName, $iisApp)

      } else {

        DBGIF 'AppCmd on Windows 2008 RTM does not support  using the ~ operator (verbs, filteringRules, denyUrlSequences, denyQueryStringSequences' { $true }
      }

      Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}/{1}" /section:system.webServer/security/requestFiltering /+"verbs.[verb=''GET'',allowed=''True'']"' -f $fsSiteName, $iisApp)
      Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}/{1}" /section:system.webServer/security/requestFiltering /+"verbs.[verb=''HEAD'',allowed=''True'']"' -f $fsSiteName, $iisApp)

    } else {

      DBGIF 'We do not support IIS on unknown platforms' { $true }
    }
  }
}


function global:Get-Forests ()
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  [System.Xml.XmlNodeList] $forestPromos = $null
  DBGSTART
  $forestPromos = $xmlConfig.SelectNodes(('./VMs/*/domain[@dcPromo and @new and not(@join) and not(@forest)]'))
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
  DBGIF $MyInvocation.MyCommand.Name { (Is-Null $forestPromos) -or ((Get-CountSafe $forestPromos) -lt 1) }

  [Collections.ArrayList] $forests = @()

  if ((Get-CountSafe $forestPromos) -gt 0) {

    foreach ($oneForestPromo in $forestPromos) {

      DBG ('One forest found in config: {0}' -f $oneForestPromo.new)
      [void] $forests.Add($oneForestPromo.new)
    }
  }

  return ,$forests
}


function global:New-ForestMachine ([string] $domain, [string] $hostName, [string] $fqdn, [bool] $dc, [bool] $firstDCinDomain, [string] $joinerCred, [bool] $do, [string] $upnSuffixes, [bool] $isRODC, [string] $adSite, [string] $rodcOwnGroup)
{
  $newMachine = New-Object PSCustomObject

  Add-Member -InputObject $newMachine -MemberType NoteProperty -Name do -Value $do
  Add-Member -InputObject $newMachine -MemberType NoteProperty -Name domain -Value $domain
  Add-Member -InputObject $newMachine -MemberType NoteProperty -Name hostName -Value $hostName
  Add-Member -InputObject $newMachine -MemberType NoteProperty -Name fqdn -Value $fqdn
  Add-Member -InputObject $newMachine -MemberType NoteProperty -Name isDC -Value $dc
  Add-Member -InputObject $newMachine -MemberType NoteProperty -Name isRODC -Value $isRODC
  Add-Member -InputObject $newMachine -MemberType NoteProperty -Name rodcOwnGroup -Value $rodcOwnGroup
  Add-Member -InputObject $newMachine -MemberType NoteProperty -Name isFirstDCinDomain -Value $firstDCinDomain
  Add-Member -InputObject $newMachine -MemberType NoteProperty -Name joinerCred -Value $joinerCred
  Add-Member -InputObject $newMachine -MemberType NoteProperty -Name upnSuffixes -Value $upnSuffixes
  Add-Member -InputObject $newMachine -MemberType NoteProperty -Name adSite -value $adSite

  return $newMachine
}


function global:Get-ForestMachines ([string] $forestRootOrSubDomain)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $forestRootOrSubDomain }
  DBGIF $MyInvocation.MyCommand.Name { $forestRootOrSubDomain -notlike '?*.?*' }

  [string] $reparseForest = $null

  do {

    [System.Collections.ArrayList] $forestMachines = @()

    if (Is-ValidString $reparseForest) {

      DBG ('We are reparsing the forest again, now with the found forest name instead of the previously supplied subdomain name: originalSubDomain = {0} | discoveredForest = {1}' -f $forestRootOrSubDomain, $reparseForest)
      $forestRootOrSubDomain = $reparseForest
    }

    if (($forestRootOrSubDomain -like '?*.?*.?*') -and (Is-EmptyString $reparseForest)) {

      # Note: we cannot not be sure here if the caller supplied forestRoot or a subdomain
      #       because not even the caller might know. So we just try to search for subdomain
      #       as well. Subdomain names cannot contain dots, so there cannot for example be
      #       a double subdomain such as subsub.sub .parentDomain. Thus we are safe to 
      #       try to cut only the leftmost name

      $ifSubDomain_Name = $forestRootOrSubDomain.SubString(0, $forestRootOrSubDomain.IndexOf('.')).ToLower()
      $ifSubDomain_Parent = $forestRootOrSubDomain.SubString(($forestRootOrSubDomain.IndexOf('.') + 1)).ToLower()

      DBG ('The requested domain is of at least third level, so we try to parse for a parent: {0} | {1}' -f $ifSubDomain_Name, $ifSubDomain_Parent)
      $forestDCs = $xmlConfig.SelectNodes(('./VMs/*/domain[@dcPromo!="" and (translate(@new,"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz")="{0}" or translate(@join,"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz")="{0}" or translate(@forest,"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz")="{0}" or (translate(@new,"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz")="{1}" and translate(@join,"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz")="{2}"))]' -f $forestRootOrSubDomain.ToLower(), $ifSubDomain_Name, $ifSubDomain_Parent))

    } else {

      # Note: if the domain name has a single dot, then it must be either forest root domain
      #       or a tree-top subdomain, which can also be found with the same xpath following

      $forestDCs = $xmlConfig.SelectNodes(('./VMs/*/domain[@dcPromo!="" and (translate(@new,"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz")="{0}" or translate(@join,"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz")="{0}" or translate(@forest,"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz")="{0}")]' -f $forestRootOrSubDomain.ToLower()))
    }

    DBG ('Found DCs for forest/subdomain: {0} | {1}' -f $forestRootOrSubDomain, (Get-CountSafe $forestDCs))
    $reparseForest = $null


    if ((Get-CountSafe $forestDCs) -gt 0) {

      foreach ($forestDC in $forestDCs) {

        DBG ('One forest/subdomain DC found: new = {0} | join = {1} | forest = {2}' -f $forestDC.new, $forestDC.join, $forestDC.forest)
        $foundMachine = Get-MachineFromElement $forestDC
        $foundMachineDomain = Get-MachineDomain $foundMachine
        $foundMachineHostName = $foundMachine.hostName
        $foundMachineDo = Parse-BoolSafe $foundMachine.vm.do
        $foundMachineFQDN = Get-MachineFQDN $foundMachine
        $foundMachineJoinerCred = (Get-JoinerCredentials $foundMachine).FullLogin
        $isFirstDC = $false
        $isRODC = Is-NonNull $forestDC.rodc
        [string] $rodcOwnGroup = $null
        
        if ($isRODC) {
        
          $rodcOwnGroup = $forestDC.rodc.ownGroup
        }
        
        [string] $adSite = $null
        if (Is-ValidString $forestDC.adsite.site) {

          $adSite = $forestDC.adsite.site
        }


        if ((Is-ValidString $forestDC.new) -and (Is-EmptyString $forestDC.join)) {

          # This means forest root domain first DC itself
          DBGIF $MyInvocation.MyCommand.Name { $forestDC.new -ne $forestRootOrSubDomain }

          $isFirstDC = $true
      
        } elseif ((Is-ValidString $forestDC.new) -and ($forestDC.new -notlike '*.*') -and (Is-ValidString $forestDC.join)) {

          # Pure subdomain of some upper domain, first DC
          DBGIF $MyInvocation.MyCommand.Name { ($forestDC.join -ne $forestRootOrSubDomain) -and (Is-EmptyString $forestDC.forest) }
        
          if ((Is-ValidString $forestDC.forest) -and ($forestDC.forest -ne $forestRootOrSubDomain)) {

            # Note: if the caller supplied subdomain name instead of the whole forest
            #       we must reparse the machines for the whole forest instead

            DBGIF $MyInvocation.MyCommand.Name { Is-ValidString $reparseForest }
            $reparseForest = $forestDC.forest
          }

          $isFirstDC = $true

        } elseif ((Is-ValidString $forestDC.new) -and ($forestDC.new -like '*.*') -and (Is-ValidString $forestDC.join)) {

          # New tree in the forest, first DC
          DBGIF $MyInvocation.MyCommand.Name { ($forestDC.join -ne $forestRootOrSubDomain) -and (Is-EmptyString $forestDC.forest) }
          DBGIF $MyInvocation.MyCommand.Name { (Is-ValidString $forestDC.forest) -and ($forestDC.forest -ne $forestRootOrSubDomain) }

          $isFirstDC = $true

        } elseif (Is-EmptyString $forestDC.new) {

          # Secondary DC in an existing domain
          $isFirstDC = $false
      
        } else {

          DBGIF 'Invalid DC''s domain setting' { $true }
        }

        [void] $forestMachines.Add((New-ForestMachine $foundMachineDomain $foundMachineHostName $foundMachineFQDN $true $isFirstDC $foundMachineJoinerCred $foundMachineDo $foundMachine.domain.upn -isRODC $isRODC -adSite $adSite -rodcOwnGroup $rodcOwnGroup))
      }
    }

  } while (Is-ValidString $reparseForest)


  [string] $domainXPath = $null
  foreach ($oneUniqueDomain in ($forestMachines | Select-Object -Unique domain | Select-Object -Expand domain)) {

    if (Is-ValidString $domainXPath) {

      $domainXPath += ' or '
    }

    $domainXPath += 'translate(@join,"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz")="{0}"' -f $oneUniqueDomain.ToLower()
  }


  $forestMembers = $xmlConfig.SelectNodes('./VMs/*/domain[(not(@dcPromo) or @dcPromo="") and ({0})]' -f $domainXPath)
  DBG ('Found member machines for forest: {0} | {1}' -f $forestRootOrSubDomain, (Get-CountSafe $forestMembers))

  if ((Get-CountSafe $forestMembers) -gt 0) {

    foreach ($forestMember in $forestMembers) {

      $foundMachine = Get-MachineFromElement $forestMember
      $foundMachineDomain = Get-MachineDomain $foundMachine
      $foundMachineHostName = $foundMachine.hostName
      $foundMachineDo = Parse-BoolSafe $foundMachine.vm.do
      $foundMachineFQDN = Get-MachineFQDN $foundMachine
      $foundMachineJoinerCred = (Get-JoinerCredentials $foundMachine).FullLogin
      
      [string] $foundMachineSite = $null
      if (Is-ValidString $foundMachine.domain.adsite.site) {

        $foundMachineSite = $foundMachine.domain.adsite.site
      }

      [void] $forestMachines.Add((New-ForestMachine $foundMachineDomain $foundMachineHostName $foundMachineFQDN $false $false $foundMachineJoinerCred $foundMachineDo $null -isRODC $false -adSite $foundMachineSite))
    }
  }

  DBG ('Returning forest machines: {0} | {1}' -f $forestMachines.Count, ($forestMachines | ft * | Out-String))

  return $forestMachines
}


function global:Is-FirstForestDC ([System.Xml.XmlElement] $vmXml)
{
  return ((Is-ValidString $vmXml.domain.new) -and (Is-EmptyString $vmXml.domain.join))
}


function global:Is-FirstDomainDC ([System.Xml.XmlElement] $vmXml)
{
  return (Is-ValidString $vmXml.domain.new)
}


function global:Get-FirstDC ([string] $domainFQDN)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { (Is-EmptyString $domainFQDN) }

  [string] $firstDC = ''

  if (Is-ValidString $domainFQDN) {

    DBG ('Determine first DC of the domain: {0}' -f $domainFQDN)
    DBGSTART
    $firstDCs = $global:xmlConfig.SelectNodes('./VMs/MACHINE[vm/@do="true"]/domain[@dcPromo and (translate(@new,"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz")="{0}" or concat(translate(@new,"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz"), ".", translate(@join,"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz"))="{0}")]' -f $domainFQDN.ToLower())
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    DBGIF ('Found more than a single first DC for a domain: {0} | {1}' -f $domainFQDN, ($firstDCs | Out-String)) { (Get-CountSafe $firstDCs) -gt 1 }
    DBGIF ('No first DC found for a domain: {0}' -f $domainFQDN) { (Get-CountSafe $firstDCs) -eq 0 }

    if ((Get-CountSafe $firstDCs) -ge 1) {

      $firstDC = (Get-MachineFromElement $firstDCs.Item(0)).hostName
    }
  }

  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $firstDC }

  DBG ('First DC found: {0}' -f $firstDC)
  return $firstDC
}


function global:Get-AllAppHostsOfInstance ([string] $appTag, [string] $instance, [string] $hostType, [string] $onlyBeforeMyHostName)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  
  $normalInstance = $instance.ToLower()
  
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $appTag }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $normalInstance }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $hostType }

  [System.Collections.ArrayList] $hosts = @()

  if ((Is-ValidString $appTag) -and (Is-ValidString $normalInstance) -and ($normalInstance -ne 'local')) {

    if (Is-EmptyString $onlyBeforeMyHostName) {

      DBG ('Determine ALL app hosts of the instance: {0} | {1}' -f $appTag, $normalInstance)
      DBGSTART
      $appHosts = $global:xmlConfig.SelectNodes(('./VMs/MACHINE[vm/@do="true"]/{0}[translate(@instance,"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz")="{1}"]/..' -f $appTag, $normalInstance))
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND

    } else {

      DBG ('Determine ALL app hosts of the instance BEFORE the one specified: {0} | {1} | before = {2}' -f $appTag, $normalInstance, $onlyBeforeMyHostName)
      DBGSTART
      $appHosts = $global:xmlConfig.SelectNodes(('./VMs/MACHINE[vm/@do="true" and following-sibling::MACHINE[@hostName="{0}"]]/{1}[translate(@instance,"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz")="{2}"]/..' -f $onlyBeforeMyHostName, $appTag, $normalInstance))
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
    }

  } else {

    $appHosts = @($global:vmConfig)
  }

  DBG ('Base app hosts found: {0}' -f (Get-CountSafe $appHosts))
  DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $appHosts) -lt 1 }

  if ((Get-CountSafe $appHosts) -gt 0) {

    foreach ($oneAppHost in $appHosts) {
 
      DBG ('Get the hostType from the apphost: {0} | {1}' -f $hostType, $oneAppHost.hostName)
      $oneHost = $null

      if ($hostType -eq 'waitParams') {
  
        $oneHost = Get-MachineWaitParams $oneAppHost
        DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $oneHost }
        DBG ('One app host wait params: {0}' -f $oneHost)

      } elseif ($hostType -eq 'fqdn') {

        $oneHost = Get-MachineFQDN $oneAppHost
        DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $oneHost }
        DBG ('One app host FQDN name: {0}' -f $oneHost)

      } elseif ($hostType -eq 'vmConfig') {

        $oneHost = $oneAppHost
        DBGIF $MyInvocation.MyCommand.Name { Is-Null $oneHost }
        DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $oneHost.hostName }
        DBG ('One app host name: {0}' -f $oneHost.hostName)
      
      } elseif ($hostType -eq 'svcConfig') {

        $oneHost = $oneAppHost.SelectSingleNode(('./{0}[translate(@instance,"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz")="{1}"]' -f $appTag, $normalInstance))
        DBGIF $MyInvocation.MyCommand.Name { Is-Null $oneHost }
      
      } else {

        DBGIF ('Invalid host type') { $true }
      }

      [void] $hosts.Add($oneHost)
    }
  }


  DBG ('App hosts found: {0} | {1}' -f $hosts.Count, ($hosts -join ', '))

  return ,$hosts
}


function global:Get-FirstAppHostInInstance ([string] $appTag, [string] $instance, [string] $hostType, [System.Xml.XmlElement] $someElementIfInstanceEmpty, [bool] $getLastHostInstead)
# hostType - waitParams | fqdn | vmConfig | svcConfig
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  
  $normalInstance = $instance.ToLower()

  DBGIF $MyInvocation.MyCommand.Name { (Is-EmptyString $appTag) }
  #DBGIF $MyInvocation.MyCommand.Name { (Is-EmptyString $normalInstance) } # instance is empty on all applications except when it is referencing something like 
  DBGIF $MyInvocation.MyCommand.Name { (Is-EmptyString $hostType) }

  [object] $firstAppHost = ''
  $firstAppVM = $null

  if ((Is-ValidString $appTag) -and (Is-ValidString $normalInstance) -and ($normalInstance -ne 'local')) {

    if (-not $getLastHostInstead) {

      DBG ('Determine the FIRST app host of the instance: {0} | {1}' -f $appTag, $normalInstance)
      DBGSTART
      $firstAppVM = $global:xmlConfig.SelectSingleNode(('./VMs/MACHINE[vm/@do="true"]/{0}[translate(@instance,"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz")="{1}"]/..' -f $appTag, $normalInstance))
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
    
    } else {

      DBG ('Determine the LAST app host of the instance: {0} | {1}' -f $appTag, $normalInstance)
      DBGSTART
      $firstAppVM = $global:xmlConfig.SelectSingleNode(('(./VMs/MACHINE[vm/@do="true"]/{0}[translate(@instance,"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz")="{1}"]/..)[last()]' -f $appTag, $normalInstance))
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
    }

  } elseif (Is-NonNull $someElementIfInstanceEmpty) {

    # Note: there are two cases when this function can be called
    #       a) from machine/app buildup itself, so we would proceed with $global:vmConfig automatically
    #       b) for example from DNS app record builder, where we need to get the $oneApp's VM instead

    $firstAppVM = Get-MachineFromElement $someElementIfInstanceEmpty

  } else {

    $firstAppVM = $global:vmConfig
  }

  DBG ('First App VM found: {0}' -f $firstAppVM.hostName)

  if ($hostType -eq 'waitParams') {
  
    $firstAppHost = Get-MachineWaitParams $firstAppVM

  } elseif ($hostType -eq 'fqdn') {

    $firstAppHost = Get-MachineFQDN $firstAppVM

  } elseif ($hostType -eq 'vmConfig') {

    $firstAppHost = $firstAppVM
  
  } elseif ($hostType -eq 'svcConfig') {

    # Note: we could have more instances of the same service on a single machine
    $svcXPath = './{0}[translate(@instance,"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz")="{1}"]' -f $appTag, $normalInstance
    DBG ('Will search for the service instance with the following filter: {0}' -f $svcXPath)
    $firstAppHost = $firstAppVM.SelectSingleNode($svcXPath)
  }


  DBG ('First App host result: {0} | {1}' -f $firstAppVM.hostName, ($firstAppHost | Out-String))
  return $firstAppHost
}


function global:Check-FirstAppHostInInstance ([string] $appTag, [string] $instance)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { (Is-EmptyString $appTag) }
  DBGIF $MyInvocation.MyCommand.Name { (Is-EmptyString $instance) }

  DBG ('Get first App instance host: {0} | {1}' -f $appTag, $instance)
  $firstAppHostFQDN = Get-FirstAppHostInInstance $appTag $instance 'fqdn'

  DBG ('Get wait params for the current App host')
  $thisAppHostFQDN = Get-MachineFQDN $global:vmConfig

  [bool] $res = ($firstAppHostFQDN -eq $thisAppHostFQDN)
  DBG ('Is this the first App instance host: {0}' -f $res)
  
  return $res
}


function global:Check-LastAppHostInInstance ([string] $appTag, [string] $instance)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { (Is-EmptyString $appTag) }
  DBGIF $MyInvocation.MyCommand.Name { (Is-EmptyString $instance) }

  DBG ('Get last App instance host: {0} | {1}' -f $appTag, $instance)
  $firstAppHostFQDN = Get-FirstAppHostInInstance $appTag $instance 'fqdn' -getLastHostInstead $true

  DBG ('Get wait params for the current App host')
  $thisAppHostFQDN = Get-MachineFQDN $global:vmConfig

  [bool] $res = ($firstAppHostFQDN -eq $thisAppHostFQDN)
  DBG ('Is this the last App instance host: {0}' -f $res)
  
  return $res
}


function global:Get-MyPrivateIPs ()
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  $allValidNICs = Get-WMIQueryArray '.' $global:wmiFltValidNIC 'root/CIMv2' $false

  [string[]] $foundIPs = @()

  $foundIPs = $allValidNICs | ? { (Is-IPAddressPrivate $_.IPAddress) } | Select-Object -Expand IPAddress | ? { Is-IPAddressPrivate $_ }
  $resultIPs = Format-MultiValue $foundIPs

  DBG ('Private IPs found on the local computer: {0}' -f $resultIPs)

  return $resultIPs
}


function global:Get-MyStaticIPs ()
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  $allValidNICs = Get-WMIQueryArray '.' $global:wmiFltValidNIC 'root/CIMv2' $false

  [string[]] $foundIPs = @()

  $foundIPs = $allValidNICs | ? { ($_.DHCPEnabled -eq $false) -and (Is-Null $_.DHCPServer) } | Select-Object -Expand IPAddress
  $resultIPs = Format-MultiValue $foundIPs

  DBG ('Static IPs found on the local computer: {0}' -f $resultIPs)

  return $resultIPs
}


function global:Get-MyAllIPs ()
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  $allValidNICs = Get-WMIQueryArray '.' $global:wmiFltValidNIC 'root/CIMv2' $false

  [string[]] $foundIPs = @()

  $foundIPs = $allValidNICs | Select-Object -Expand IPAddress
  $resultIPs = Format-MultiValue $foundIPs

  DBG ('All IPs found on the local computer: {0}' -f $resultIPs)

  return $resultIPs
}


function global:Resolve-DNSNameWithConfig ([string] $dnsName, [System.Xml.XmlElement] $dnsAppNode)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { -not ((Is-EmptyString $dnsName) -or ($dnsName -match $global:rxDns) -or ($dnsName -eq $global:emptyValueMarker)) }
  DBGIF $MyInvocation.MyCommand.Name { -not ((($dnsName -eq $global:emptyValueMarker) -and (Is-NonNull $dnsAppNode)) -or ((Is-ValidString $dnsName) -and (Is-Null $dnsAppNode)) -or ((Is-EmptyString $dnsName) -and (Is-NonNull $dnsAppNode))) }
  DBGIF $MyInvocation.MyCommand.Name { -not ((Is-Null $dnsAppNode) -or (Is-ValidString $dnsAppNode.dns) -or ($dnsName -eq $global:emptyValueMarker)) }

  [PSCustomObject] $outAddress = New-Object PSCustomObject
  Add-Member -InputObject $outAddress -MemberType NoteProperty -Name ipAddress -Value ([string] $null)
  Add-Member -InputObject $outAddress -MemberType NoteProperty -Name cnameFQDN -Value ([string] $null)

#  $outAddressTemp = $null
#
#  if (Is-Null $dnsAppNode) {
#
#    # Note: what if the DNS records needed are not yet created on the respective DNS server?
#    DBGIF 'We cannot be sure to wait for DNS translation on non-domain computers' { -not (Is-LocalComputerMemberOfDomain) }
#    DBGIF $MyInvocation.MyCommand.Name { $returnCNAME }
#
#    DBGSTART
#    $outAddressTemp = [System.Net.Dns]::Resolve($dnsName)
#    $outAddress.ipAddress = $outAddressTemp.AddressList[0].IPAddressToString
#    DBGER $MyInvocation.MyCommand.Name $error
#    DBGEND
#
#    DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $outAddressTemp.AddressList) -gt 1 } 
#  }


 # if (Is-EmptyString $outAddress.ipAddress) {

    # Note the cases for missing CNAMEs - the other machine not yet registered itself, or we are rebuilding the DNS server as well
#    DBGIF ('The specified DNS name is CNAME alias: {0}. Note that it may be prone to be missing yet.' -f (Format-MultiValue $outAddressTemp.Aliases)) { (Get-CountSafe $outAddressTemp.Aliases.Count) -gt 0 }

#    DBG ('Attempt obtaining the DNS/IP address from our config')


  if (Is-Null $dnsAppNode) {

    DBG ('Must find an app that requires the queried dns name first.')

    $possibleDnsAppNodes = $xmlConfig.SelectNodes('./VMs/MACHINE[vm/@do="true"]/*/*[contains(translate(@dns,"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz"), "{0}")]' -f $dnsName.ToLower())
    DBG ('Found possible DNS app nodes: {0}' -f (Get-CountSafe $possibleDnsAppNodes))

    if ((Get-CountSafe $possibleDnsAppNodes) -gt 0) {

      foreach ($possibleDnsAppNode in $possibleDnsAppNodes) {

        $possibleDnsRecords = Split-MultiValue $possibleDnsAppNode.dns
        DBG ('One possible DNS app node: {0}' -f ($possibleDnsRecords -join ','))

        foreach ($possibleDnsRecord in $possibleDnsRecords) {

          if ((Strip-ValueFlags $possibleDnsRecord) -eq $dnsName) {

            DBGIF $MyInvocation.MyCommand.Name { -not ((Is-Null $dnsAppNode) -or ($dnsAppNode.instance -eq $possibleDnsAppNode.instance)) }
            $dnsAppNode = $possibleDnsAppNode
          }
        }
      }
    }

    DBGIF $MyInvocation.MyCommand.Name { Is-Null $dnsAppNode }
  }


  if (Is-NonNull $dnsAppNode) {

    DBG ('We have the DNS app node, get its machine element')

    if ($dnsName -eq $global:emptyValueMarker) {

      DBG ('The DNS app node specified is meant to be just for this machine regardless of instance')
      $appHostNode = Get-MachineFromElement $dnsAppNode

    } else {

      DBG ('The DNS app node specified is meant to provide us with its instance name')
      # @dns is either requested on an  element where .instance is empty because it referes to the local instance (while .instance is on its parent node)
      # or the application requests the @dns on for example  element referencing some other instance on possibly other machine
      $appHostNode = Get-FirstAppHostInInstance $dnsAppNode.name $dnsAppNode.instance 'vmConfig' $dnsAppNode
    }


    $outAddress.cnameFQDN = Get-MachineFQDN $appHostNode
    
    DBG ('IP addresses from config: {0}' -f $appHostNode.net.ip)
    $outAddress.ipAddress = (Split-MultiValue $appHostNode.net.ip) | % { Strip-ValueFlags $_ } | ? { $_ -ne $global:emptyValueMarker } | Select -First 1
  }

  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $outAddress.ipAddress }
  DBGIF $MyInvocation.MyCommand.Name { (Is-ValidString $outAddress.ipAddress) -and (-not (Is-IPv4OrIPv6Address $outAddress.ipAddress)) }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $outAddress.cnameFQDN }
  DBGIF $MyInvocation.MyCommand.Name { (Is-ValidString $outAddress.cnameFQDN) -and ($outAddress.cnameFQDN -notmatch $global:rxDns) -and ($outAddress.cnameFQDN -notlike '*.*') }

  DBG ('Resolved IP address and CNAME: {0} | {1}' -f $outAddress.ipAddress, $outAddress.cnameFQDN)

  return $outAddress
}


function global:New-NetworkMapElement ([string] $hvNetwork, [string] $nicName, [string] $ipAddress, [string] $mask, [string] $gateway, [string] $machine, [bool] $enabledByDefault, [string] $nlbInstance)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  #DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $hvNetwork } # Note: might be empty if the machine is built manually in an existing VM for example
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $nicName }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $ipAddress }
  DBGIF $MyInvocation.MyCommand.Name { (Is-ValidString $ipAddress) -and ($ipAddress -ne $global:emptyValueMarker) -and ($ipAddress -notmatch (RxFullStr $global:rxIPv4)) }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $mask }
  DBGIF $MyInvocation.MyCommand.Name { (Is-ValidString $mask) -and ($mask -ne $global:emptyValueMarker) -and ($mask -notmatch (RxFullStr $global:rxIPv4Mask)) }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $gateway }
  DBGIF $MyInvocation.MyCommand.Name { (Is-ValidString $gateway) -and ($gateway -ne $global:emptyValueMarker) -and ($gateway -notmatch (RxFullStr $global:rxIPv4)) }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $machine }

  DBGIF $MyInvocation.MyCommand.Name { ($ipAddress -eq $global:emptyValueMarker) -and ($mask -ne $global:emptyValueMarker) }
  DBGIF $MyInvocation.MyCommand.Name { ($ipAddress -eq $global:emptyValueMarker) -and ($gateway -ne $global:emptyValueMarker) }
  DBGIF $MyInvocation.MyCommand.Name { ($ipAddress -ne $global:emptyValueMarker) -and ($mask -eq $global:emptyValueMarker) }
  
  [PSCustomObject] $netmapElement = New-Object PSObject
  
  [string] $subnetBits = $null
  [string] $subnetIP = $null

  if ((Is-ValidString $mask) -and ($mask -ne $global:emptyValueMarker)) {

    $subnetBits = Get-IPSubnetBits $mask
    $subnetIP = Get-IPSubnet $ipAddress $mask
  }

  Add-Member -Input $netmapElement -MemberType NoteProperty -Name hvNetwork -Value $hvNetwork
  Add-Member -Input $netmapElement -MemberType NoteProperty -Name nicName -Value $nicName
  Add-Member -Input $netmapElement -MemberType NoteProperty -Name ipAddress -Value $ipAddress
  Add-Member -Input $netmapElement -MemberType NoteProperty -Name mask -Value $mask
  Add-Member -Input $netmapElement -MemberType NoteProperty -Name subnetBits -Value $subnetBits
  Add-Member -Input $netmapElement -MemberType NoteProperty -Name subnetIP -Value $subnetIP
  Add-Member -Input $netmapElement -MemberType NoteProperty -Name gateway -Value $gateway
  Add-Member -Input $netmapElement -MemberType NoteProperty -Name machine -Value $machine
  Add-Member -Input $netmapElement -MemberType NoteProperty -Name enabledByDefault -Value $enabledByDefault
  Add-Member -Input $netmapElement -MemberType NoteProperty -Name nlbInstance -Value $nlbInstance
  
  return $netmapElement
}


function global:Get-NetworkMap ([string] $onlyOneMachineHostName)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { Is-Null $global:xmlConfig }

  [Collections.ArrayList] $networkMap = @()

  if (Is-EmptyString $onlyOneMachineHostName) {

    $allMachines = $global:xmlConfig.SelectNodes('./VMs/MACHINE[net/@nicNames]')
 
  } else {

    $allMachines = $global:xmlConfig.SelectNodes(('./VMs/MACHINE[translate(@hostName,"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz")="{0}" and net/@nicNames]' -f $onlyOneMachineHostName.ToLower()))
  }

  DBG ('Found machines in the config: {0}' -f (Get-CountSafe $allMachines))
  if ((Get-CountSafe $allMachines) -gt 0) {

    foreach ($oneMachine in $allMachines) {

      DBG ('Going to map machine: {0}' -f $oneMachine.hostName)
      [object[]] $nicConfig = Get-NICsListFromXmlConfig $oneMachine

      if ((Get-CountSafe $nicConfig) -gt 0) {

        foreach ($oneNicConfig in $nicConfig) {
          
          [bool] $enabledByDefault = -not (Has-ValueFlags $oneNicConfig.name D)
          #$oneNicIPs = Split-MultiValue $oneNicConfig.IPs

          DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $global:ipMultivalueSeparator }
          [string[]] $oneNicIPs = $oneNicConfig.IPs.Split($global:ipMultivalueSeparator)
          [string[]] $oneNicMasks = $oneNicConfig.masks.Split($global:ipMultivalueSeparator)
          [string[]] $oneNicGateways = $oneNicConfig.gateways.Split($global:ipMultivalueSeparator)

          DBGIF $MyInvocation.MyCommand.Name { ($oneNicIPs.Count -ne $oneNicMasks.Count) -or ($oneNicIPs.Count -lt $oneNicGateways.Count) -or ($oneNicGateways.Count -lt 1) }

          [int] $gwIndexer = 0
          for ($i = 0; $i -lt $oneNicIPs.Count; $i ++) {
                    
            [void] $networkMap.Add((New-NetworkMapElement (Strip-ValueFlags $oneNicConfig.hvNetwork) (Strip-ValueFlags $oneNicConfig.name) (Strip-ValueFlags $oneNicIPs[$i]) (Strip-ValueFlags $oneNicMasks[$i]) (Strip-ValueFlags $oneNicGateways[$gwIndexer]) $oneMachine.hostName $enabledByDefault))

            if ($gwIndexer -lt ($oneNicGateways.Count - 1)) {

              $gwIndexer ++
            }
          }
        }
      }


      DBG ('Check if any NLB IPs need to be included: {0}' -f $oneMachine.hostName)
      $nlbRules = $oneMachine.SelectNodes('.//nlbrule[@instance]')
      DBG ('The machine uses NLB: {0} | {1}' -f $oneMachine.hostName, ((Get-CountSafe $nlbRules) -gt 0))

      [string[]] $nlbInstances = @()
      if ((Get-CountSafe $nlbRules) -gt 0) {

        $nlbInstances = $nlbRules | Select -Unique instance | Select -Expand instance
        DBG ('The machines uses the following NLB instances: #{0} | {1}' -f $nlbInstances.Length, ($nlbInstances -join ','))
      }        

      # Note: although it is quite uncommon, but we must assume that some service is requesting
      #       a NLB rule in a different machine's NLB instance while this machine also has a different
      #       NLB instance to provision. Or this machine could have a NLB instance which no one
      #       requests a rule at all
      if (Is-ValidString $oneMachine.nlb.instance) {

        $nlbInstances = ($nlbInstances + $oneMachine.nlb.instance) | select -Unique
      }

      DBG ('Overall we have NLB instances for this machine: #{0} | {1}' -f $nlbInstances.Length, ($nlbInstances -join ','))
      foreach ($oneNlbInstance in $nlbInstances) {

        DBG ('Get the NLB instance parameters: {0}' -f $oneNlbInstance)
        $nlbInstanceNodes = $global:xmlConfig.SelectNodes(('./VMs/MACHINE/nlb[@ip and translate(@instance,"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz")="{0}"]' -f $oneNlbInstance.ToLower()))
        DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $nlbInstanceNodes) -ne 1 }

        if ((Get-CountSafe $nlbInstanceNodes) -ge 1) {

          $theNlbInstanceNode = Obtain-ListIdx $nlbInstanceNodes

          [string] $nlbIP = $theNlbInstanceNode.ip
          DBGIF ('Weird NLB IP: {0}' -f $nlbIP) { -not (Is-IPv4OrIPv6Address $nlbIP) }
          [string] $nlbMask = $theNlbInstanceNode.mask
          [string] $nlbIPSubnet = Get-IPSubnet $nlbIp $nlbMask
          DBG ('Find an appropriate NIC to match the NLB IP: {0} | {1} | {2}' -f $nlbIP, $nlbMask, $nlbIPSubnet)

          [object[]] $nlbNICs = $networkMap | ? { ($_.machine -eq $oneMachine.hostName) -and ($_.subnetIP -eq $nlbIPSubnet) }
          DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $nlbNICs) -eq 0 }
          DBGIF ('Ambiguous NIC/NLB mapping: {0}' -f (($nlbNICs | select -Expand nicName) -join ',')) { (Get-CountSafe $nlbNICs) -gt 1 }
          DBG ('NLB maps to NIC: {0} | {1}' -f $nlbNICs[0].nicName, $nlbNICs[0].ipAddress)

          [void] $networkMap.Add((New-NetworkMapElement $nlbNICs[0].hvNetwork $nlbNICs[0].nicName $nlbIP $nlbMask '-' $oneMachine.hostName $true $theNlbInstanceNode.instance))
        }
      }
    }
  }

  DBG ("Returning network map elements: {0} | -->`r`n{1}" -f $networkMap.Count, ($networkMap | ft -Auto | Out-String))
  DBGIF $MyInvocation.MyCommand.Name { ((Get-CountSafe $allMachines) -gt 0) -and ($networkMap.Count -lt 1) }

  return $networkMap
}


function global:Wait-Service ([string] $service)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  $waitCount = 1
  $waitMax = 50 # Note: on a heavily loaded hosts, the installation of USBoverNetwork takes even up to 196 seconds
  $waitDelay = 7
  $waitElapsed = 0

  # Note:
  #   Although SilentlyContinue does not produce any output, it still puts the error messages into the $error collection
  #   In PowerShell 3.0 there has been a new value "Ignore" introduced, but it is not available with older PowerShell versions
  #   Thus we have to clear the $error collection manually to prevent DBGSTART Outstanding Errors from appearing.
  DBGSTART
  while (((Get-Service -Name $service -EA SilentlyContinue).Status -ne 'Running') -and ($waitCount -le $waitMax)) { 
  
    DBG ('Service not yet running: {0} | attempt {1} of {2} | {3} sec.' -f $service, $waitCount, $waitMax, $waitDelay)

    Start-Sleep $waitDelay

    $waitCount ++
    $waitElapsed += $waitDelay
  }
  DBGEND
  
  DBGIF ('Waiting for the service INTERRUPTED: {0}' -f $service) { $waitCount -gt $waitMax }
  DBG ('Finished waiting for the service: {0} | {1} sec.' -f $service, $waitElapsed)
}


function global:Wait-CertSvc ()
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  $waitCount = 1
  $waitMax = 15
  $waitDelay = 7
  $waitElapsed = 0

  while (((Run-Process 'CERTUTIL' '-ping' $false $null $null $null $false $null $true) -ne 0) -and ($waitCount -le $waitMax)) { 
  
    DBG ('CertSvc DCOM service not yet running: attempt {0} of {1} | {2} sec.' -f $waitCount, $waitMax, $waitDelay)

    Start-Sleep $waitDelay

    $waitCount ++
    $waitElapsed += $waitDelay
  }
  
  DBGIF 'Waiting for the CertSvc DCOM service INTERRUPTED' { $waitCount -gt $waitMax }
  DBG ('Finished waiting for the CertSvc DCOM service: {0} sec.' -f $waitElapsed)
}


function global:Install-USBoverNetwork ([string] $clientOrServer, [string] $usbServerFQDN, [bool] $noAutoConnect)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  $shouldInstall = $true

  if ($clientOrServer -eq 'server') {

    #$productCode = '{36E00174-043B-4074-B95F-DD750C8A84A8}'
    #$usbVersion = '4-7-4'
    $productCode = '{0AA6AA5E-9201-436F-A7A3-A97159354AB9}'
    $usbVersion = '5-0-2'
    #$productCode = '{0B00E0F0-10E3-449D-845D-43FA409D2F26}'
    #$usbVersion = '5-1-11'
    
    # The documentation says that to install the Server, you must use ADDLOCAL=ALL REMOVE=cln 
    # instead of just documenting all the ADDLOCAL things. So I do as the documentation says
    # Yes, one could always go for ORCA and debug all the other things, but I go rather the documente
    # way instead
    $clientOrServerSpecRemove = 'Drivers_cln_f'

  } else {

    #$productCode = '{BF8CF2E8-D471-4741-B608-8D008B10A227}'
    #$usbVersion = '4-7-4'
    $productCode = '{0AA6AA5E-9201-436F-A7A3-A97159354AB9}'
    $usbVersion = '5-0-2'
    #$productCode = '{0B00E0F0-10E3-449D-845D-43FA409D2F26}'
    #$usbVersion = '5-1-11'
    $clientOrServerSpecRemove = 'Drivers_srv_f'
  }


  DBG ('Check if USB over Network {0} is already installed' -f $clientOrServer)
  if ($global:thisOSVersionNumber -eq 5.1) {

    DBGIF 'Cannot query Win32_Product on Windows XP' { $true }

  } else {

    $usbSoftwarePresent = Get-WMIQuerySingleObject '.' ('SELECT * FROM Win32_Product WHERE IdentifyingNumber = "{0}" AND AssignmentType = 1' -f $productCode)

    if (Is-NonNull $usbSoftwarePresent) {

      DBG ('USB over Network {0} is already installed. Skipping. {1}' -f $clientOrServer, ($usbSoftwarePresent | Out-String))
      $shouldInstall = $false
    }
  }


  DBG ('Should we really install: {0}' -f $shouldInstall)
  if ($shouldInstall) {

    #if ($global:thisOSVersionNumber -eq 6.0) {

      # There is a difference on Windows Vista where the TrustedPublishers cannot get validated 
      # when the chain is not fully trusted.
      #Install-CertificateAndChain (Join-Path $global:rootDir ('USBoverNetwork\usb-over-network-{0}*.cer' -f $usbVersion)) 'Cert:\LocalMachine\TrustedPublisher'

    #} else {

      Install-CertificateAndChain (Join-Path $global:rootDir ('USBoverNetwork\usb-over-network-{0}*.cer' -f $usbVersion)) 'Cert:\LocalMachine\TrustedPublisher'
    #}
  

    DBG ('Install USB over Network MSI: {0} | {1}' -f $clientOrServer, $usbVersion)

    #$usbOverNetworkMSI = Join-Path $global:rootDir ('ForeignBinaries\USBoverNetwork\usb-over-network-{0}-{1}.msi' -f $clientOrServer, $usbVersion)
    $usbOverNetworkMSI = Join-Path $global:rootDir ('ForeignBinaries\USBoverNetwork\usb-over-network-client-server-{0}-{1}.msi' -f $usbVersion, $global:thisOSArchitecture.Replace('-', ''))

    $usbOverNetworkMSILog = Get-DataFileApp ('usb-over-network-{0}' -f $clientOrServer) $null '.log'
    
    if ($global:thisOSArchitecture -ne '64-bit') {

      $usbOverNetwrokSETTINGS = 'SHOW_SPLASH=0 CHECK_FOR_NEW_VERSION=0'
    
    } else {

      # there is a bug in 64bit installer which cannot write into \Wow6432Node\Software
      # instead of the \Software\Wow6432Node when these options are specified
      $usbOverNetwrokSETTINGS = ''
    }

    Run-Process 'msiexec' ('/i "{0}" {3} OPEN_URL=0 ALLUSERS=1 ADDLOCAL=ALL REMOVE={1} -qn -lv* "{2}"' -f $usbOverNetworkMSI, $clientOrServerSpecRemove, $usbOverNetworkMSILog, $usbOverNetwrokSETTINGS)

    DBG ('Wait until the USB service is running')

    if ($clientOrServer -eq 'server') {

      Wait-Service ftusbsrv

    } else {

      Wait-Service ftusbsrvc
      #Restart-Service ftusbsrvc
      #Wait-Service ftusbsrvc
    }
 
    if ($global:thisOSVersion -like '5.*') {

      DBG ('Give it some 25 sec. to install USBD.SYS assynchronously on Win 5.x')
      Start-Sleep 25

      # Not necessary with 5.0.x USBoverNetwork version anymore
      #DBG ('Repair USB over Network shortcut on Win 5.x')
      #Copy-Item -Path (Join-Path $global:rootDir ('USBoverNetwork\USB over Network {0}.lnk' -f $clientOrServer)) -Destination "$env:ALLUSERSPROFILE\Start Menu\Programs" -Force -EV er -EA SilentlyContinue
      #DBGER $MyInvocation.MyCommand.Name $er
  
    } else {

      # Not necessary with 5.0.x USBoverNetwork version anymore
      #DBG ('Repair USB over Network shortcut on Win 6.x')
      #Copy-Item -Path (Join-Path $global:rootDir ('USBoverNetwork\USB over Network {0}.lnk' -f $clientOrServer)) -Destination "$env:ProgramData\Microsoft\Windows\Start Menu\Programs" -Force -EV er -EA SilentlyContinue
      #DBGER $MyInvocation.MyCommand.Name $er
    }
  }

  DBG ('Should we setup USB server connection on client: {0}' -f (($clientOrServer -eq 'client') -and (Is-ValidString $usbServerFQDN)))
  if (($clientOrServer -eq 'client') -and (Is-ValidString $usbServerFQDN)) {

    <# Does not work with 5.0.2 anymore

    $usbServerFQDNorIP = (([System.Text.UnicodeEncoding]::Unicode.GetBytes($usbServerFQDN) | % { '{0:X2}' -f $_ }) -join ',')

    DBG ('Define USB server connection: {0} | {1}' -f $usbServerFQDN, $usbServerFQDNorIP)

    if ($noAutoConnect) {

      $regFileName = 'usb-over-network-client-connection-noAutoconnect'

    } else {

      $regFileName = 'usb-over-network-client-connection'
    }

    $usbServerConnFile = Get-DataFileApp $regFileName $null '.reg'
    $argReplacement = 'usbServerFQDNorIP${0}' -f $usbServerFQDNorIP
    Replace-ArgumentsInFile (Join-Path $global:rootDir "USBoverNetwork\$regFileName.reg") $argReplacement $usbServerConnFile Unicode
    Run-Process 'REGEDIT' ('/S "{0}"' -f $usbServerConnFile)

    #>

    foreach ($oneUsbServerFQDN in (Split-MultiValue $usbServerFQDN)) {

      DBG ('Adding USB server connection: {0}' -f $oneUsbServerFQDN)
      Run-Process "$env:ProgramFiles\USB over Network\usbclncmd.exe" "add $oneUsbServerFQDN"
    }
  }

  ##
  ReDisable-SystemRestore
  ReDisable-Updates
}


function global:Get-CharacteristicIntFromFirstMACAddress ()
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  [int] $outInt = -1
  
  [string] $firstNICMAC = (Get-WMIQueryArray '.' 'SELECT * FROM Win32_NetworkAdapter WHERE MacAddress LIKE "%:%:%:%:%:%"')[0].MacAddress.Replace(':', '')

  DBG ('First NIC MAC address: {0}' -f $firstNICMAC)

  $sha = New-Object System.Security.Cryptography.SHA1CryptoServiceProvider
  $hashBytes = $sha.ComputeHash([System.Text.ASCIIEncoding]::ASCII.GetBytes($firstNICMAC))

  DBG ('Hash computed: {0}' -f ($hashBytes -join '-'))

  DBGSTART
  $outInt = [BitConverter]::ToInt32($hashBytes, 16)
  DBGER $MyInvocation.MyCommand.Name $Error
  DBGEND

  DBG ('Characteristic INT computed: {0}' -f $outInt)

  return $outInt
}


function global:Validate-GPOBackup ([string] $rootFldr = (Join-Path $global:libCommonParentDir GPO-Backup))
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $rootFldr }
  DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path $rootFldr) }

  if ((Is-ValidString $rootFldr) -and (Test-Path $rootFldr)) {

    $bckXml = Load-XmlSafe $rootFldr\manifest.xml #[XML] (cat $rootFldr\manifest.xml)
    $gpoFldrs = Get-ChildItem $rootFldr\{*-*-*-*-*}

    $backupCount = Get-CountSafe $bckXml.Backups.BackupInst
    $fldrCount = Get-CountSafe $gpoFldrs

    DBG ('Found backups: gpos = {0} | fldrs = {1}' -f $backupCount, $fldrCount)
    DBGIF ('Inconsistent number of GPO backups: gpos = {0} | fldrs = {1}' -f $backupCount, $fldrCount) { $backupCount -ne $fldrCount }

    [System.Collections.ArrayList] $foundList = @()

    DBG ('Compile MANIFEST.XML contents')
    foreach ($oneBackup in $bckXml.Backups.BackupInst) {

      $guid = $oneBackup.gpoguid.'#cdata-section'
      $id = $oneBackup.id.'#cdata-section'
      $dispName = $oneBackup.gpodisplayName.'#cdata-section'

      $oneInfo = New-Object PSCustomObject

      Add-Member -Input $oneInfo -MemberType NoteProperty -Name Guid -Value $guid
      Add-Member -Input $oneInfo -MemberType NoteProperty -Name Id -Value $id
      Add-Member -Input $oneInfo -MemberType NoteProperty -Name Display -Value $dispName
      Add-Member -Input $oneInfo -MemberType NoteProperty -Name Fldr -Value $rootFldr\$id
      Add-Member -Input $oneInfo -MemberType NoteProperty -Name Exists -Value (Test-Path $oneInfo.Fldr)
    
      [void] $foundList.Add($oneInfo)  
    }


    DBG ('Compile folder contents')
    foreach ($oneFldr in $gpoFldrs) {

      $fldrId = Split-Path $oneFldr.FullName -Leaf

      if (-not (Contains-Safe $foundList $fldrId Id)) {

        $oneInfo = New-Object PSCustomObject

        Add-Member -Input $oneInfo -MemberType NoteProperty -Name Guid -Value $null
        Add-Member -Input $oneInfo -MemberType NoteProperty -Name Id -Value $fldrId
        Add-Member -Input $oneInfo -MemberType NoteProperty -Name Display -Value $null
        Add-Member -Input $oneInfo -MemberType NoteProperty -Name Fldr -Value $oneFldr.FullName
        Add-Member -Input $oneInfo -MemberType NoteProperty -Name Exists -Value (Test-Path $oneInfo.Fldr)
    
        [void] $foundList.Add($oneInfo)  
      }
    }

    $invalidList = $foundList | ? { (-not $_.Exists) -or (Is-EmptyString $_.Display) }


    DBG ('Found items: {0}' -f ($foundList | sort Display | ft Display, Exists, Id | Out-String))
    DBG ('Invalid items: {0} | {1}' -f (Get-CountSafe $invalidList), ($invalidList | sort Display | ft Display, Exists, Id | Out-String))

    DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $invalidList) -gt 0 }

    DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe (Find-DuplicatesInSortedList ($foundList | sort Display) Display)) -gt 0 }
  }
}


function global:Get-WindowsSXSOfflineMedia ([string] $installMediaVolume, [string] $installISOVolume)
{
  [string] $offlineSxsMedia = $null

  #$versionNumber = [string]::Format([CultureInfo]::InvariantCulture, $global:thisOSVersionNumber, '{0:N1}')
  DBGIF $MyInvocation.MyCommand.Name { $global:thisOSVersionNumber -lt 6 }

  if ($global:thisOSArchitecture -eq '32-bit') {

    if ($global:thisOSVersionNumber -ge 10) {

      $offlineSxsMedia = Join-Path $installMediaVolume ('WindowsSXS\{0}\SXS-x32' -f $global:thisOSVersion)

    } else {

      $offlineSxsMedia = Join-Path $installMediaVolume ('WindowsSXS\{0}\SXS-x32' -f $global:thisOSVersionShort)
    }

  } else {

    if ($global:thisOSVersionNumber -ge 10) {

      $offlineSxsMedia = Join-Path $installMediaVolume ('WindowsSXS\{0}\SXS-x64' -f $global:thisOSVersion)

    } else {

      $offlineSxsMedia = Join-Path $installMediaVolume ('WindowsSXS\{0}\SXS-x64' -f $global:thisOSVersionShort)
    }
  }

  if ((Is-ValidString $installISOVolume) -and (-not (Test-Path $offlineSxsMedia))) {

    DBG ('The offline media does not exist on installMediaVolume, we can try finding it on ISO volume')
    $offlineSxsMedia = Join-Path $installISOVolume 'sources\sxs'
  }

  DBG ('Returning the following SXS offline media path: {0}' -f $offlineSxsMedia)
  DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path $offlineSxsMedia) }

  return $offlineSxsMedia
}


function global:Detect-NetFxVersions ()
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBG ('List NDP registry keys and assert')
  DBGSTART
  [Microsoft.Win32.RegistryKey[]] $ndpKeys = Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP'
  DBGER $MyInvocation.MyCommand.Name $Error
  DBGEND
  DBG ('Found NDP registry keys: #{0} | {1}' -f (Get-CountSafe $ndpKeys), (($ndpKeys | select -Expand Name) -join ','))
  DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $ndpKeys) -lt 1 }

  if ((Get-CountSafe $ndpKeys) -gt 0) {

    foreach ($oneNdpKey in $ndpKeys) {

      [string] $keyName = Split-Path -Leaf $oneNdpKey.Name
      DBGIF ('Unknown NETFX NDP key found: {0} | {1}' -f $keyName, $oneNdpKey.Name) { -not (Contains-Safe @('CDF', 'v1.1.4322', 'v2.0.50727', 'v3.0', 'v3.5', 'v4', 'v4.0') $keyName) }
    }
  }

  DBG ('Load some detection values in safe manner')
  
  ##
  ##
  DBGSTART

  [Microsoft.Win32.RegistryKey[]] $netfx11Keys = Get-Item -Path 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v1.1.*'
  DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $netfx11Keys) -gt 1 }
  
  [string] $netFx11VersionStr = $null
  [string] $netFx11InstallationVersionStr = $null
  [int] $netFx11Install = 0
  if ((Get-CountSafe $netfx11Keys) -gt 0) {

    $netFx11InstallationVersionStr = (Split-Path -Leaf $netfx11Keys[0].Name).Substring(1)
    $netFx11VersionStr = $netFx11InstallationVersionStr
    $netFx11Install = (Get-ItemProperty -Path $netfx11Keys[0].Name.Replace('HKEY_LOCAL_MACHINE', 'HKLM:') -Name Install).Install
  }

  [Microsoft.Win32.RegistryKey[]] $netfx20Keys = Get-Item -Path 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v2.0.*'
  DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $netfx20Keys) -gt 1 }
  
  [string] $netFx20VersionStr = $null
  [string] $netFx20InstallationVersionStr = $null
  [int] $netFx20Install = 0
  if ((Get-CountSafe $netfx20Keys) -gt 0) {

    $netFx20InstallationVersionStr = (Split-Path -Leaf $netfx20Keys[0].Name).Substring(1)
    $netFx20VersionStr = (Get-ItemProperty -Path $netfx20Keys[0].Name.Replace('HKEY_LOCAL_MACHINE', 'HKLM:') -Name Version).Version
    $netFx20Install = (Get-ItemProperty -Path $netfx20Keys[0].Name.Replace('HKEY_LOCAL_MACHINE', 'HKLM:') -Name Install).Install
  }

  [string] $netFx30VersionStr = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v3.0' -Name Version).Version
  [int] $netFx30Install = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v3.0' -Name Install).Install

  [string] $netFx35VersionStr = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v3.5' -Name Version).Version
  [int] $netFx35Install = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v3.5' -Name Install).Install

  [string] $netFx4FullVersionStr = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full' -Name Version).Version
  [int] $netFx4FullInstall = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full' -Name Install).Install

  [string] $netFx4ClientVersionStr = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Client' -Name Version).Version
  [int] $netFx4ClientInstall = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Client' -Name Install).Install

  DBGEND
  ##
  ##

  DBGIF ('Weird NETFX11 registry: {0} | {1}' -f $netFx11VersionStr, $netFx11Install) { (Is-ValidString $netFx11VersionStr) -xor ($netFx11Install -eq 1) }
  DBGIF ('Weird NETFX11 registry: {0}' -f $netFx11Install) { ($netFx11Install -ne 0) -and ($netFx11Install -ne 1) }

  DBGIF ('Weird NETFX20 registry: {0} | {1}' -f $netFx20VersionStr, $netFx20Install) { (Is-ValidString $netFx20VersionStr) -xor ($netFx20Install -eq 1) }
  DBGIF ('Weird NETFX20 registry: {0}' -f $netFx20Install) { ($netFx20Install -ne 0) -and ($netFx20Install -ne 1) }
  
  DBGIF ('Weird NETFX30 registry: {0} | {1}' -f $netFx30VersionStr, $netFx30Install) { (Is-ValidString $netFx30VersionStr) -xor ($netFx30Install -eq 1) }
  DBGIF ('Weird NETFX30 registry: {0}' -f $netFx30Install) { ($netFx30Install -ne 0) -and ($netFx30Install -ne 1) }
  
  DBGIF ('Weird NETFX35 registry: {0} | {1}' -f $netFx35VersionStr, $netFx35Install) { (Is-ValidString $netFx35versionStr) -xor ($netFx35Install -eq 1) }
  DBGIF ('Weird NETFX35 registry: {0}' -f $netFx35Install) { ($netFx35Install -ne 0) -and ($netFx35Install -ne 1) }

  DBGIF ('Weird NETFX4 registry: {0} | {1}' -f $netFx4FullVersionStr, $netFx4FullInstall) { (Is-ValidString $netFx4FullVersionStr) -xor ($netFx4FullInstall -eq 1) }
  DBGIF ('Weird NETFX4 registry: {0}' -f $netFx4FullInstall) { ($netFx4FullInstall -ne 0) -and ($netFx4FullInstall -ne 1) }
  
  [Version] $netfx11version = Parse-VersionSafe $netFx11VersionStr
  [Version] $netfx11versionBaseInstallation = Parse-VersionSafe $netFx11InstallationVersionStr
  [Version] $netfx20version = Parse-VersionSafe $netFx20VersionStr
  [Version] $netfx20versionBaseInstallation = Parse-VersionSafe $netFx20InstallationVersionStr
  [Version] $netfx30version = Parse-VersionSafe $netFx30VersionStr
  [Version] $netfx35version = Parse-VersionSafe $netFx35VersionStr
  [Version] $netfx4version = Parse-VersionSafe $netFx4FullVersionStr

  DBGIF ('Weird NETFX11 version: {0}' -f $netFx11VersionStr) { (Is-ValidString $netFx11VersionStr) -and (-not (Is-ValidVersion $netfx11version)) }
  DBGIF ('Weird NETFX11 version: {0} | {1}' -f $netFx11VersionStr, $netFx11InstallationVersionStr) { (Is-ValidVersion $netfx11version) -xor (Is-ValidVersion $netfx11versionBaseInstallation) }
  DBGIF ('Weird NETFX11 version: {0} | {1}' -f $netFx11VersionStr, $netFx11InstallationVersionStr) { (Is-ValidVersion $netfx11version) -and ($netfx11versionBaseInstallation -gt $netfx11version) }

  DBGIF ('Weird NETFX20 version: {0}' -f $netFx20VersionStr) { (Is-ValidString $netFx20VersionStr) -and (-not (Is-ValidVersion $netfx20version)) }
  DBGIF ('Weird NETFX20 version: {0} | {1}' -f $netFx20VersionStr, $netFx20InstallationVersionStr) { (Is-ValidVersion $netfx20version) -xor (Is-ValidVersion $netfx20versionBaseInstallation) }
  DBGIF ('Weird NETFX20 version: {0} | {1}' -f $netFx20VersionStr, $netFx20InstallationVersionStr) { (Is-ValidVersion $netfx20version) -and ($netfx20versionBaseInstallation -gt $netfx20version) }

  DBGIF ('Weird NETFX30 version: {0}' -f $netFx30VersionStr) { (Is-ValidString $netFx30VersionStr) -and (-not (Is-ValidVersion $netfx30version)) }
  DBGIF ('Weird NETFX35 version: {0}' -f $netFx35VersionStr) { (Is-ValidString $netFx35VersionStr) -and (-not (Is-ValidVersion $netfx35version)) }
  DBGIF ('Weird NETFX4 version: {0}' -f $netFx4FullVersionStr) { (Is-ValidString $netFx4FullVersionStr) -and (-not (Is-ValidVersion $netfx4version)) }

  DBGIF ('Unknown NETFX4 version: {0}' -f $netfx4version) { (Is-ValidVersion $netfx4version) -and (($netfx4version.Major -ne 4) -or (($netfx4version.Minor -ne 5) -and ($netfx4version.Minor -ne 6) -and ($netfx4version.Minor -ne 7))) }

  #
  #

  $netfxVersions = New-Object PSObject

  Add-Member -Input $netfxVersions -MemberType NoteProperty -Name v11EngineInstalled -Value ($netFx11Install -eq 1)
  Add-Member -Input $netfxVersions -MemberType NoteProperty -Name v20EngineInstalled -Value (Is-ValidString $netFx20VersionStr)
  Add-Member -Input $netfxVersions -MemberType NoteProperty -Name v40EngineInstalled -Value (Is-ValidString $netFx4FullVersionStr)

  Add-Member -Input $netfxVersions -MemberType NoteProperty -Name v11Version -Value $netfx11version
  Add-Member -Input $netfxVersions -MemberType NoteProperty -Name v20Version -Value $netfx20version
  Add-Member -Input $netfxVersions -MemberType NoteProperty -Name v30Version -Value $netfx30version
  Add-Member -Input $netfxVersions -MemberType NoteProperty -Name v35Version -Value $netfx35version
  Add-Member -Input $netfxVersions -MemberType NoteProperty -Name v40Version -Value $netfx4version
  Add-Member -Input $netfxVersions -MemberType NoteProperty -Name v45Version -Value $netfx4version
  Add-Member -Input $netfxVersions -MemberType NoteProperty -Name v46Version -Value $netfx4version
  Add-Member -Input $netfxVersions -MemberType NoteProperty -Name v47Version -Value $netfx4version

  Add-Member -Input $netfxVersions -MemberType NoteProperty -Name v30UpdateInstalled -Value (Is-ValidString $netFx30VersionStr)
  Add-Member -Input $netfxVersions -MemberType NoteProperty -Name v35UpdateInstalled -Value (Is-ValidString $netFx35VersionStr)
  Add-Member -Input $netfxVersions -MemberType NoteProperty -Name v45UpdateInstalled -Value ($netfx4version.Minor -ge 5)
  Add-Member -Input $netfxVersions -MemberType NoteProperty -Name v46UpdateInstalled -Value ($netfx4version.Minor -ge 6)
  Add-Member -Input $netfxVersions -MemberType NoteProperty -Name v47UpdateInstalled -Value ($netfx4version.Minor -ge 7)

  DBGIF $MyInvocation.MyCommand.Name { $netfxVersions.v11EngineInstalled -and ($global:thisOSVersionNumber -ge 6.1) }

  DBGIF $MyInvocation.MyCommand.Name { $netfxVersions.v30UpdateInstalled -and (-not $netfxVersions.v20EngineInstalled) }

  DBGIF $MyInvocation.MyCommand.Name { $netfxVersions.v35UpdateInstalled -and (-not $netfxVersions.v20EngineInstalled) }
  DBGIF $MyInvocation.MyCommand.Name { $netfxVersions.v35UpdateInstalled -and (-not $netfxVersions.v30UpdateInstalled) }

  DBGIF $MyInvocation.MyCommand.Name { $netfxVersions.v45UpdateInstalled -and (-not $netfxVersions.v40EngineInstalled) }
  DBGIF $MyInvocation.MyCommand.Name { $netfxVersions.v46UpdateInstalled -and (-not $netfxVersions.v40EngineInstalled) }
  DBGIF $MyInvocation.MyCommand.Name { $netfxVersions.v47UpdateInstalled -and (-not $netfxVersions.v40EngineInstalled) }

  DBGIF $MyInvocation.MyCommand.Name { (-not $netfxVersions.v40EngineInstalled) -and ((Is-ValidString $netFx4ClientVersionStr) -or ($netFx4ClientInstall -ne 0)) }

  #
  #

  [string] $v11path32 = $null
  [string] $v11path64 = $null
  [string] $v20path32 = $null
  [string] $v20path64 = $null
  [string] $v40path32 = $null
  [string] $v40path64 = $null

  if ($netfxVersions.v11EngineInstalled) {

    $v11path32 = '{0}\Microsoft.NET\Framework\v{1}.{2}.{3}' -f $env:windir, $netfx11versionBaseInstallation.Major, $netfx11versionBaseInstallation.Minor, $netfx11versionBaseInstallation.Build
    
    if ($global:thisOSArchitecture -ne '32-bit') {

      # Note: netfx 1.1 x64 does not exist, just assert the fact here
      $v11path64 = '{0}\Microsoft.NET\Framework64\v{1}.{2}.{3}' -f $env:windir, $netfx11versionBaseInstallation.Major, $netfx11versionBaseInstallation.Minor, $netfx11versionBaseInstallation.Build
    }

    DBGIF ('NETFX 11 x32 not installed correctly: {0}' -f $v11path32) { -not (Test-Path "$v11path32\mscorlib.dll") }
    DBGIF ('NETFX 11 x64 not installed correctly: {0}' -f $v11path64) { ($global:thisOSArchitecture -ne '32-bit') -and (Test-Path "$v11path64\mscorlib.dll") }

    DBGIF ('Other NETFX 11 x32 folders found') { (Get-CountSafe (Get-ChildItem ('{0}\Microsoft.NET\Framework\v1.1*' -f $env:windir))) -ne 1 }
    DBGIF ('Some NETFX 11 x64 folders found') { ($global:thisOSArchitecture -ne '32-bit') -and ((Get-CountSafe (Get-ChildItem ('{0}\Microsoft.NET\Framework64\v1.1*' -f $env:windir))) -ne 0) }

    $v11path64 = $null
  }

  if ($netfxVersions.v20EngineInstalled) {

    $v20path32 = '{0}\Microsoft.NET\Framework\v{1}.{2}.{3}' -f $env:windir, $netfx20versionBaseInstallation.Major, $netfx20versionBaseInstallation.Minor, $netfx20versionBaseInstallation.Build

    if ($global:thisOSArchitecture -ne '32-bit') {

      $v20path64 = '{0}\Microsoft.NET\Framework64\v{1}.{2}.{3}' -f $env:windir, $netfx20versionBaseInstallation.Major, $netfx20versionBaseInstallation.Minor, $netfx20versionBaseInstallation.Build
    }

    DBGIF ('NETFX 20 x32 not installed correctly: {0}' -f $v20path32) { -not (Test-Path "$v20path32\mscorlib.dll") }
    DBGIF ('NETFX 20 x64 not installed correctly: {0}' -f $v20path64) { ($global:thisOSArchitecture -ne '32-bit') -and (-not (Test-Path "$v20path64\mscorlib.dll")) }

    DBGIF ('Other NETFX 20 x32 folders found') { (Get-CountSafe (Get-ChildItem ('{0}\Microsoft.NET\Framework\v2.0*' -f $env:windir))) -ne 1 }
    DBGIF ('Other NETFX 20 x64 folders found') { ($global:thisOSArchitecture -ne '32-bit') -and ((Get-CountSafe (Get-ChildItem ('{0}\Microsoft.NET\Framework64\v2.0*' -f $env:windir))) -ne 1) }
  }

  if ($netfxVersions.v40EngineInstalled) {

    if ($global:thisOSArchitecture -ne '32-bit') {

      DBGSTART
      $v40path64 = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full' -Name InstallPath).InstallPath
      $v40path32 = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\NET Framework Setup\NDP\v4\Full' -Name InstallPath).InstallPath
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND

    } else {

      DBGSTART
      $v40path32 = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full' -Name InstallPath).InstallPath
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
    }

    DBGIF ('NETFX 40 x32 not installed correctly: {0}' -f $v40path32) { -not (Test-Path "$v40path32\mscorlib.dll") }
    DBGIF ('NETFX 40 x64 not installed correctly: {0}' -f $v40path64) { ($global:thisOSArchitecture -ne '32-bit') -and (-not (Test-Path "$v40path64\mscorlib.dll")) }

    DBGIF ('Other NETFX 40 x32 folders found') { (Get-CountSafe (Get-ChildItem ('{0}\Microsoft.NET\Framework\v4.0*' -f $env:windir))) -ne 1 }
    DBGIF ('Other NETFX 40 x64 folders found') { ($global:thisOSArchitecture -ne '32-bit') -and ((Get-CountSafe (Get-ChildItem ('{0}\Microsoft.NET\Framework64\v4.0*' -f $env:windir))) -ne 1) }
  }

  Add-Member -Input $netfxVersions -MemberType NoteProperty -Name v11path32 -Value $v11path32.TrimEnd('\')
  Add-Member -Input $netfxVersions -MemberType NoteProperty -Name v11path64 -Value $v11path64.TrimEnd('\')
  Add-Member -Input $netfxVersions -MemberType NoteProperty -Name v20path32 -Value $v20path32.TrimEnd('\')
  Add-Member -Input $netfxVersions -MemberType NoteProperty -Name v20path64 -Value $v20path64.TrimEnd('\')
  Add-Member -Input $netfxVersions -MemberType NoteProperty -Name v40path32 -Value $v40path32.TrimEnd('\')
  Add-Member -Input $netfxVersions -MemberType NoteProperty -Name v40path64 -Value $v40path64.TrimEnd('\')

  #
  #

  DBG ("NETFX versions detected: -->`r`n{0}" -f ($netfxVersions | fl * | Out-String))

  return $netfxVersions
}


function global:Install-WindowsFeaturesUniversal ([string[]] $inFeatures, [bool] $canRestart, [string] $installMediaVolume, [string] $installISOVolume)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $inFeatures) -le 0 }
 

  # Note: this function assumes that the restartRequired is returned
  #       immediatelly after a restart condition is reached at any point and the 
  #       function will be simply run again completelly after the reboot
  #       This means, all the previously done installations will run
  #       again, which is assumed to pass quickly and not require restart once
  #       again.
  #       If there is any other exception during the code, we should return
  #       $false in order to prevent infinite looping through reboots followed
  #       by the same exception condition.

  [bool] $restartRequired = $false
  [string] $restartRequiredException = 'Sevecek-Restart-Required'


try {

  if ((Get-CountSafe $inFeatures) -gt 0) {

    DBG ("Features REQUESTED to be installed: #{0} = {1}" -f $inFeatures.Count, ($inFeatures -join ';'))

    
    #=========================
    DBG ('Adjust feature set for .NET Framework (NetFx)')

<#    
.NET Framework (NetFx) version/compatibility matrix

2.0 - because of PowerShell 2.0, NET Framework 2.0 is base disk requirement

----------------------
NET-Framework-Core:

    2008 R1 - 3.0 (builtin)
    2008 R2 - 3.5.1 (builtin)
    2012 - 3.5 (from source media)
    2012 R2 - 3.5 (from source media)
    2016 - 3.5 (from source media)


----------------------
NET-Framework-45-Core

    2012/8 - 4.5 (builtin)


----------------------
NET Framework versions available: 2.0, 3.0, 3.5, 4.0, 4.5

3.0 = 3.0 + 2.0
3.5 = 3.5 + 3.0 + 2.0 (remain visible as all installed, but 3.0/2.0 cannot be uninstalled before 3.5)
4.0 = 4.0 (does not bring anything else with it)
4.5 = 4.0-OverwrittenBy-4.5 (4.5 actually changes name/version of the package to 4.5)
#>

    # if any framework is requested, it always ends up at v35 or v45

    if (Contains-Safe $inFeatures 'Sevecek-NetFx-20') {

      DBG ('Replace/Add Sevecek-NetFx-20 with Sevecek-NetFx-35')
      $inFeatures += 'Sevecek-NetFx-35'
    }

    if (Contains-Safe $inFeatures 'Sevecek-NetFx-30') {

      DBG ('Replace/Add Sevecek-NetFx-30 with Sevecek-NetFx-35')
      $inFeatures += 'Sevecek-NetFx-35'
    }

    if (Contains-Safe $inFeatures 'Sevecek-NetFx-40') {

      DBG ('Replace/Add Sevecek-NetFx-40 with Sevecek-NetFx-45')
      $inFeatures += 'Sevecek-NetFx-45'
    }

    if (Contains-Safe $inFeatures 'NET-Framework-Core') {

      DBG ('Replace/Add NET-Framework-Core with Sevecek-NetFx-35')
      $inFeatures += 'Sevecek-NetFx-35'
    }

    if (Contains-Safe $inFeatures 'NET-Framework-45-Core') {

      DBG ('Replace/Add NET-Framework-45-Core with Sevecek-NetFx-45')
      $inFeatures += 'Sevecek-NetFx-45'
    }

    # at this point, Sevecek-NetFx contains all the really required NetFx versions
    # but it may also contain some obsolette versions that need to be removed

    DBG ('Build OS feature list from what was REQUIRED')
    [System.Collections.ArrayList] $features = @()
    [System.Collections.ArrayList] $sevecekFeatures = @()

    # Note: Sort-Object -Unique does case INsensitive sorting and uniquation
    $inFeatures | Sort-Object -Unique | % {

      if (($_ -notlike 'Sevecek-*') -and ($_ -notlike 'NET-Framework*-Core')) { $features += $_ }
      if (($_ -like 'Sevecek-*') -and ($_ -ne 'Sevecek-NetFx-20') -and ($_ -ne 'Sevecek-NetFx-30') -and ($_ -ne 'Sevecek-NetFx-40')) { $sevecekFeatures.Add($_) | Out-Null }
    }

    # at this point, $sevecekFeatures contains at most NetFx-35 and NetFx-45
    # and $features does not contain any NetFx feature
    # so we are going to remap $sevecekFeatures to OS builtin features if any

    if ($global:thisOSVersionNumber -lt 6) {

      # nothing happens. Install both NetFx versions from Sevecek custom standalone installers
    }

    if ($global:thisOSVersionNumber -eq 6.0) {

      if (Contains-Safe $sevecekFeatures 'Sevecek-NetFx-35') {
  
        $features += 'NET-Framework-Core' # NetFx30
        # and install all other frameworks from Sevecek custom standalone installers
      }
    }

    if ($global:thisOSVersionNumber -ge 6.1) {
    
      if (Contains-Safe $sevecekFeatures 'Sevecek-NetFx-35') {
  
        $features += 'NET-Framework-Core' # NetFx35
        $sevecekFeatures.Remove('Sevecek-NetFx-35')
      }
    }
    
    if ($global:thisOSVersionNumber -ge 6.2) {

      if (Contains-Safe $sevecekFeatures 'Sevecek-NetFx-35') {
  
        $features += 'NET-Framework-Core' # NetFx35
        $sevecekFeatures.Remove('Sevecek-NetFx-35')
      }

      if (Contains-Safe $sevecekFeatures 'Sevecek-NetFx-45') {
  
        $features += 'NET-Framework-45-Core'
        $sevecekFeatures.Remove('Sevecek-NetFx-45')
      }
    }


    if ((($global:thisOSVersionNumber -eq 6.0) -or ($global:thisOSVersionNumber -eq 6.1)) -and (Contains-Safe $features 'NET-Framework-Core')) {
  
      DBG ('Removing the built in NetFx from 6.0/6.1 installation')
      $features.Remove('NET-Framework-Core')
    }
    
    if (($global:thisOSVersionNumber -ge 6.2) -and (Contains-Safe $features 'NET-Framework-45-Core')) {
  
      DBG ('Removing the built in NetFx from 6.2+ installation')
      $features.Remove('NET-Framework-45-Core')
    }
    

    if (($global:thisOSVersionNumber -le 6.3) -and (Contains-Safe $features 'Sevecek-Desktop-Experience')) {

      DBG ('Including Desktop-Experience as requested for 6.x systems by the Sevecek-Desktop-Experience')
      DBGIF ('Cannot install Desktop-Experience on Windows 5.x') { $global:thisOSVersionNumber -lt 6 }
      $features += 'Desktop-Experience'
    }

    if (($global:thisOSVersionNumber -ge 10.0) -and (Contains-Safe $features 'Sevecek-Desktop-Experience')) {

      DBG ('The Desktop-Experience is not present on Windows 2016 anymore, so just include what remained from it as instructed by the Sevecek-Desktop-Experience')
      # Note: Disk Cleanup, Sync Center, Snipping Tool, Character Map, Windows Media Player, ... are included out-of-the-box
      $features += 'WebDAV-Redirector'
    }



    DBG ('OS requested features: {0} | {1}' -f (Get-CountSafe $features), ($features | Out-String))
    DBG ('Sevecek additional custom features: {0} | {1}' -f (Get-CountSafe $sevecekFeatures), ($sevecekFeatures | Out-String))


    $netfxVersionInfo = Detect-NetFxVersions

    <#
    DBG ('Detect NetFx versions')
    DBGSTART
    $netFx35Version = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v3.5' -Name Version -EA SilentlyContinue).Version
    [bool] $netFx35Installed = ($netFx35Version -like '3.5.*') -or ($netFx35Version -like '3.5')
    DBGEND
    DBG ('NetFx 35 already installed: {0} | {1}' -f $netFx35Installed, $netFx35Version)
    DBGIF ('Weird NetFx 35 version: {0}' -f $netFx35Version) { (Is-ValidString $netFx35Version) -and (-not $netFx35Installed) }

    DBGSTART
    $netFx45Version = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full' -Name Version -EA SilentlyContinue).Version
    [bool] $netFx45Installed = ($netFx45Version -like '4.5.*') -or ($netFx45Version -like '4.5') -or ($netFx45Version -like '4.6.*') -or ($netFx45Version -like '4.7.*')
    DBGEND
    DBG ('NetFx 45 already installed: {0} | {1}' -f $netFx45Installed, $netFx45Version)
    DBGIF ('Weird NetFx 45 version: {0}' -f $netFx45Version) { (Is-ValidString $netFx45Version) -and (-not $netFx45Installed) -and ($netFx45Version -notlike '4.0.*') }
    #>

    [bool] $netFx35Installed = $netfxVersionInfo.v35UpdateInstalled
    [bool] $netFx45Installed = $netfxVersionInfo.v45UpdateInstalled
    [bool] $netFx47Installed = $netfxVersionInfo.v47UpdateInstalled

    #=========================
    # DotNetFx should install first as it does not require restart while
    # on some system do not install well after system features that require restart
    # themselves (such as win2008 rtm)
    DBG ('Install Sevecek custom features: #{0} | {1}' -f (Get-CountSafe $sevecekFeatures), ($sevecekFeatures -join ','))

    if (Contains-Safe $sevecekFeatures 'Sevecek-NetFx-35') {

      if (-not $netFx35Installed) {

        DBG ('Install NetFx 35 from standalone installer')
        $installExeReturnCode = Run-Process (Join-Path $installMediaVolume 'DotNetFx\dotnetfx35.exe') '/q /norestart' -returnExitCode $true
        DBGIF ('Unexpected installation return code: {0}' -f $installExeReturnCode) { ($installExeReturnCode -ne 0) -and ($installExeReturnCode -ne $global:win32_ERROR_SUCCESS_REBOOT_REQUIRED) }

        if ($installExeReturnCode -eq $global:win32_ERROR_SUCCESS_REBOOT_REQUIRED) {

          $restartRequired = $true
          throw $restartRequiredException
        }
      }
    }

    if (Contains-Safe $sevecekFeatures 'Sevecek-NetFx-45') {

      if (-not $netFx45Installed) {

        DBG ('Install NetFx 45 from standalone installer')
        $installExeReturnCode = Run-Process (Join-Path $installMediaVolume 'DotNetFx\dotNetFx45_Full_x86_x64.exe') '/q /norestart' -returnExitCode $true
        DBGIF ('Unexpected installation return code: {0}' -f $installExeReturnCode) { ($installExeReturnCode -ne 0) -and ($installExeReturnCode -ne $global:win32_ERROR_SUCCESS_REBOOT_REQUIRED) }

        if ($installExeReturnCode -eq $global:win32_ERROR_SUCCESS_REBOOT_REQUIRED) {
        
          $restartRequired = $true
          throw $restartRequiredException
        }
      }
    }

    if (Contains-Safe $sevecekFeatures 'Sevecek-NetFx-472') {

      if (-not $netFx47Installed) {

        DBG ('Install NetFx 472 from standalone installer')
        $installExeReturnCode = Run-Process (Join-Path $installMediaVolume 'DotNetFx\dotNetFx472_Full_x86_x64.exe') '/q /norestart' -returnExitCode $true
        DBGIF ('Unexpected installation return code: {0}' -f $installExeReturnCode) { ($installExeReturnCode -ne 0) -and ($installExeReturnCode -ne $global:win32_ERROR_SUCCESS_REBOOT_REQUIRED) }

        if ($installExeReturnCode -eq $global:win32_ERROR_SUCCESS_REBOOT_REQUIRED) {
        
          $restartRequired = $true
          throw $restartRequiredException
        }
      }
    }


    #=========================
    DBG ('Install base OS features')

    if ((Get-CountSafe $features) -gt 0) {

      DBG ('Going to process OS builtin features')

      if ($global:thisOSVersionNumber -eq 6.0) {

        DBG ("Doing 6.0 feature installation.")
         
        $otherOptions = ''
        if ($canRestart) { $otherOptions = '{0} -restart' -f $otherOptions }

        $srvmgrLog = Get-DataFileApp "servermanager" $null '.log'
        $featureList = $features -join ' '    
        # Note: on Win6.0 the features must be separated by a space
        #       yet not surrounded with quotes, just let as separate parameters
        $installExeReturnCode = Run-Process 'servermanagercmd' ('-install {0} -allSubFeatures -logPath "{1}" {2}' -f $featureList, $srvmgrLog, $otherOptions) -returnExitCode $true
        # status 1003 - all features already installed or already removed
        DBGIF ('Unexpected installation return code: {0}' -f $installExeReturnCode) { ($installExeReturnCode -ne 0) -and ($installExeReturnCode -ne $global:win32_ERROR_SUCCESS_REBOOT_REQUIRED) -and ($installExeReturnCode -ne 1003)}

        if ($installExeReturnCode -eq $global:win32_ERROR_SUCCESS_REBOOT_REQUIRED) {
         
          $restartRequired = $true
          throw $restartRequiredException
        }
      }


      if ($global:thisOSVersionNumber -eq 6.1) {
      
        DBG ("Doing 6.1 feature installation.")

        $otherOptions = @{}
        if ($canRestart) { $otherOptions += @{ Restart = $true } }

        DBG ('Import ServerManager')
        DBGSTART
        Import-Module ServerManager
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
        
        DBG ('Going to iterate through all the features: {0}' -f (Get-CountSafe $features))
        foreach ($oneFeature in $features) {

          DBG ('Will install one feature: {0}' -f $oneFeature)

          DBGSTART
          $theFeature = $null
          $theFeature = Get-WindowsFeature $oneFeature
          DBGER $MyInvocation.MyCommand.Name $error
          DBGEND
          DBGIF ('The feature does not exist: {0}' -f $oneFeature) { Is-Null $theFeature }

          if (Is-NonNull $theFeature) {

            DBG ('Current pre-install feature state: {0} | {1} | {2}' -f $theFeature.Name, $theFeature.DisplayName, $theFeature.InstallState)

            DBG ('Using Add-WindowsFeature')
            DBGSTART
            $instRes = $null
            $instRes = Add-WindowsFeature -Name $oneFeature -IncludeAllSubFeature @otherOptions
            DBGER $MyInvocation.MyCommand.Name $error
            DBGEND

            DBG ("Installation success: {0} | restart = {1} | {2}" -f (Parse-BoolSafe($instRes.Success)), $instRes.RestartNeeded.ToString(), $instRes.RestartNeeded.value__)
            DBGIF $MyInvocation.MyCommand.Name { -not (Parse-BoolSafe($instRes.Success)) }
            DBGIF $MyInvocation.MyCommand.Name { ($instRes.RestartNeeded.value__ -ne 1) -and ($instRes.RestartNeeded.value__ -ne 2) }
            DBGIF $MyInvocation.MyCommand.Name { ($instRes.RestartNeeded.value__ -eq 1) -and ($instRes.RestartNeeded.ToString() -ne 'No') }
            DBGIF $MyInvocation.MyCommand.Name { ($instRes.RestartNeeded.value__ -eq 2) -and ($instRes.RestartNeeded.ToString() -ne 'Yes') }

            DBGSTART
            $theFeature = $null
            $theFeature = Get-WindowsFeature $oneFeature
            DBGER $MyInvocation.MyCommand.Name $error
            DBGEND
            DBGIF ('The feature does not exist after existing previously: {0}' -f $oneFeature) { Is-Null $theFeature }
            DBG ('Current post-install feature state: {0} | {1} | {2}' -f $theFeature.Name, $theFeature.DisplayName, $theFeature.InstallState)

            if ($instRes.RestartNeeded.value__ -ne 1) {

              DBG ('Restart required by the feature: {0}' -f $oneFeature)
              $restartRequired = $true

              # Note: if we iterate through individual features one-by-one, we 
              #       must also restart after each feature that requires the restart
              throw $restartRequiredException
            }
          }
        }

        if ($restartRequired) {

          DBG ('Restart required by at least one of the features')
          throw $restartRequiredException
        }
      }

  
      if ($global:thisOSVersionNumber -ge 6.2) {
      
        DBG ("Doing 6.2+ feature installation.")

        # Note: sometimes the Web-Asp-Net can be installed on its own without explicitly requiring the NET-Framework-Core to be named among the features
        if ((Contains-Safe $features 'NET-Framework-Core') -or (Contains-Safe $features 'Web-Asp-Net')) {

          DBG ('Install NetFx3 from source media separately')
          DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $installMediaVolume }

          $netFx3Source = Get-WindowsSXSOfflineMedia $installMediaVolume $installISOVolume

          DBG ('Source media path exists: {0} | {1}' -f (Test-Path $netFx3Source), $netFx3Source)
          DBGIF ('Source media path to install NetFX3 does not exist: {0}' -f $netFx3Source) { -not (Test-Path $netFx3Source) }

          if (Test-Path $netFx3Source) {

            if ($thisOSRole -notlike '*workstation*') {

              $installExeReturnCode = Run-Process 'DISM' ('/online /enable-feature /featurename:NetFx3ServerFeatures /source:"{0}"' -f $netFx3Source) -returnExitCode $true
              DBGIF ('Unexpected installation return code: {0}' -f $installExeReturnCode) { ($installExeReturnCode -ne 0) -and ($installExeReturnCode -ne $global:win32_ERROR_SUCCESS_REBOOT_REQUIRED) }

              if ($installExeReturnCode -eq $global:win32_ERROR_SUCCESS_REBOOT_REQUIRED) {
         
                $restartRequired = $true
                throw $restartRequiredException
              }
            }


            $installExeReturnCode = Run-Process 'DISM' ('/online /enable-feature /featurename:NetFx3 /source:"{0}"' -f $netFx3Source) -returnExitCode $true
            DBGIF ('Unexpected installation return code: {0}' -f $installExeReturnCode) { ($installExeReturnCode -ne 0) -and ($installExeReturnCode -ne $global:win32_ERROR_SUCCESS_REBOOT_REQUIRED) }
            
            if ($installExeReturnCode -eq $global:win32_ERROR_SUCCESS_REBOOT_REQUIRED) {
         
              $restartRequired = $true
              throw $restartRequiredException
            }
          }
        }


        $otherOptions = @{}
        if ($canRestart) { $otherOptions += @{ Restart = $true } }

        DBG ('Proceed with the remaining non-NETFX features on 6.2+: {0} | otherOptions = {1}' -f ($features -join ';'), $otherOptions)

        if ($thisOSRole -like '*workstation*') {

          # If we require NetFx on Windows7 it is either built-in or got installed with .EXE already
          # the only colliding case comes on Windows8+ where we may ask for installation of NetFX-45 which is
          # already present and we can safelly skip the installation at all
          DBGIF 'This OS is a workstation older than 6.2 and some other features than NET-Framework-45-Core are required!' { -not (($features.Count -eq 1) -and ($thisOSVersionNumber -ge 6.2) -and (Contains-Safe $features 'NET-Framework-45-Core')) }

          DBG 'This OS is a workstation and Install-WindowsFeature cannot be used here. Skipping the installation.'
        
        } else {

          DBG ('Import ServerManager is not necessary on 6.2+')
          DBGSTART
          #Import-Module ServerManager
          DBGER $MyInvocation.MyCommand.Name $error
          DBGEND

          DBG ('Going to iterate through all the features: {0}' -f (Get-CountSafe $features))
          foreach ($oneFeature in $features) {

            DBG ('Will install one feature: {0}' -f $oneFeature)

            DBGSTART
            $theFeature = $null
            $theFeature = Get-WindowsFeature $oneFeature
            DBGER $MyInvocation.MyCommand.Name $error
            DBGEND
            DBGIF ('The feature does not exist: {0}' -f $oneFeature) { Is-Null $theFeature }

            if (Is-NonNull $theFeature) {

              DBG ('Current pre-install feature state: {0} | {1} | {2}' -f $theFeature.Name, $theFeature.DisplayName, $theFeature.InstallState)

              DBG ('Using Install-WindowsFeature')
              DBGSTART
              $instRes = $null
              $instRes = Install-WindowsFeature -Name $oneFeature -IncludeAllSubFeature -IncludeManagementTools @otherOptions -EA SilentlyContinue
              DBGER $MyInvocation.MyCommand.Name $error
              DBGEND

              DBG ("Installation success: {0} | restart = {1} | {2}" -f (Parse-BoolSafe($instRes.Success)), $instRes.RestartNeeded.ToString(), $instRes.RestartNeeded.value__)
              DBGIF $MyInvocation.MyCommand.Name { -not (Parse-BoolSafe($instRes.Success)) }
              DBGIF $MyInvocation.MyCommand.Name { ($instRes.RestartNeeded.value__ -ne 1) -and ($instRes.RestartNeeded.value__ -ne 2) }
              DBGIF $MyInvocation.MyCommand.Name { ($instRes.RestartNeeded.value__ -eq 1) -and ($instRes.RestartNeeded.ToString() -ne 'No') }
              DBGIF $MyInvocation.MyCommand.Name { ($instRes.RestartNeeded.value__ -eq 2) -and ($instRes.RestartNeeded.ToString() -ne 'Yes') }

              DBGSTART
              $theFeature = $null
              $theFeature = Get-WindowsFeature $oneFeature
              DBGER $MyInvocation.MyCommand.Name $error
              DBGEND
              DBGIF ('The feature does not exist after existing previously: {0}' -f $oneFeature) { Is-Null $theFeature }
              DBG ('Current post-install feature state: {0} | {1} | {2}' -f $theFeature.Name, $theFeature.DisplayName, $theFeature.InstallState)

              if ($instRes.RestartNeeded.value__ -ne 1) {

                DBG ('Restart required by the feature: {0}' -f $oneFeature)
                $restartRequired = $true

                #DBGIF ('Pre-release version of Windows Server 2016 must restart on every iteration which requires it: {0}' -f $oneFeature) { $global:thisOSVersionNumber -ge 10 }
                # Note: if we iterate through individual features one-by-one, we 
                #       must also restart after each feature that requires the restart
                #if ($global:thisOSVersionNumber -ge 10) {
  
                  throw $restartRequiredException
              }
              #}
            }
          }

          if ($restartRequired) {

            DBG ('Restart required by at least one of the features')
            throw $restartRequiredException
          }
        }
      }

  
      if ($thisOSVersion -like '5.*') {

        $sysocmgrTXT = "[NetOptionalComponents]`r`n"
        $features | ? { -not (Has-ValueFlags $_ 'C') } | % { $sysocmgrTXT += "{0}=1`r`n" -f (Strip-ValueFlags $_) }

        $sysocmgrTXT += "[Components]`r`n"
        $features | ? { (Has-ValueFlags $_ 'C') } | % { $sysocmgrTXT += "{0}=on`r`n" -f (Strip-ValueFlags $_) }

        DBG ("Built SYSOCMGR text file: {0}" -f $sysocmgrTXT)
    
        $sysocmgrFile = Get-DataFileApp "sysocmgr" $null '.txt'
        DBG ("Saving SYSOCMGR text file to: {0}" -f $sysocmgrFile)
        Set-Content -Path $sysocmgrFile -Value $sysocmgrTXT -Encoding Ascii -Force -EV er -EA SilentlyContinue
        DBGER $MyInvocation.MyCommand.Name $er
    
        $otherOptions = ''
        if (-not $canRestart) { $otherOptions = '{0} /r' -f $otherOptions }

        $installExeReturnCode = Run-Process 'sysocmgr' ('/i:"{0}" /u:"{1}" {2} /q' -f "$env:SystemRoot\inf\sysoc.inf", $sysocmgrFile, $otherOptions) -returnExitCode $true
        DBGIF ('Unexpected installation return code: {0}' -f $installExeReturnCode) { ($installExeReturnCode -ne 0) -and ($installExeReturnCode -ne $global:win32_ERROR_SUCCESS_REBOOT_REQUIRED) }
            
        if ($installExeReturnCode -eq $global:win32_ERROR_SUCCESS_REBOOT_REQUIRED) {
         
          $restartRequired = $true
          throw $restartRequiredException
        }
      }
    }


    ##
    ReDisable-SystemRestore
    ReDisable-Updates
  }

  DBG ('Install-WindowsFeaturesUniversal FINISHED.')
}

catch [System.Exception] {

  if (($error.Count -gt 1) -or ($error[0].Exception.Message -ne $restartRequiredException)) {

    DBGER $MyInvocation.MyCommand.Name $error
  }

  $error.Clear()
  # Cannot call DBGEND as we never DBGSTARTed
  #DBGEND
}

  return $restartRequired
}



function global:Install-ProgramPlusPrerequisites ([string] $msuWildcard, [object[]] $msuInclusions, [string] $programWildcard, [object[]] $programInclusions, [string] $programParams)
# inclusions:
#   {0} - installMediaVolume
#   {1} - osVersionNumberString
#   {2} - osBitVersion
#
# example: Install-ProgramPlusPrerequisites '{0}\InternetExplorer\v{3}-w{1}\*-{2}.msu' @($ieVersion) '{0}\InternetExplorer\v{3}-w{1}\IE{3}-Windows{1}-{2}-en-us.exe' @($ieVersion) '/quiet /closeprograms /norestart'
#
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  if ($global:thisOSArchitecture -eq '32-bit') { 

    $osBitVersion = 'x86'

  } else {

    $osBitVersion = 'x64'
  }


  if (Is-ValidString $msuWildcard) {

    DBG ('MSU wildcard specified, going to find the MSU packages')

    $msuInclusionsArray = @($global:installMediaVolume.TrimEnd('\'), (Get-OSVersionNumberString $global:thisOSVersion), $osBitVersion) + $msuInclusions
    $msuPathWildcard = $msuWildcard -f $msuInclusionsArray
    DBG ('Find the required MSU packages: {0}' -f $msuPathWildcard)

    DBGSTART
    $msuPaths = Get-ChildItem $msuPathWildcard | Select-Object -ExpandProperty FullName
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $msuPaths) -lt 1 }

    foreach ($oneMsuPath in $msuPaths) {
 
      DBG ('One required MSU: {0}' -f $oneMsuPath)
      Run-Process 'WUSA' ('"{0}" /quiet /norestart' -f $oneMsuPath)
    }
  }


  if (Is-ValidString $programWildcard) {

    DBG ('Prerequisites installed, proceed with the applications themselves')

    $prgInclusionsArray = @($global:installMediaVolume.TrimEnd('\'), (Get-OSVersionNumberString $global:thisOSVersion), $osBitVersion) + $programInclusions
    $prgPathWildcard = $programWildcard -f $prgInclusionsArray
    DBG ('Find the required program executables: {0}' -f $prgPathWildcard)

    DBGSTART
    $prgPaths = Get-ChildItem $prgPathWildcard | Select-Object -ExpandProperty FullName
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $prgPaths) -lt 1 }

    foreach ($onePrgPath in $prgPaths) {
 
      DBG ('One program to be run: {0}' -f $onePrgPath)
      Run-Process $onePrgPath $programParams
    }
  }
}


function global:Install-Update ([string] $kb, [switch] $mayNotExist)
{
  # D:\Updates\Windows6.2-KB2888853-x64.msu
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  $baseUpdatesDir = Join-Path $global:installMediaVolume Updates
  DBGIF ('Invalid updates base dir: {0}' -f $baseUpdatesDir) { -not (Test-Path -Literal $baseUpdatesDir) }

  if ($global:thisOSArchitecture -eq '64-bit') {

    $bitVersion = '64'

  } else {

    $bitVersion = '32'
  }

  if (Test-Path -Literal $baseUpdatesDir) {

    $updateOSwithoutBuild = Join-Path $baseUpdatesDir ('Windows{0}-KB{1}-x{2}.msu' -f $global:thisOSVersionShort, $kb, $bitVersion)
    DBG ('Update for this OS base version exists: {0} | {1}' -f (Test-Path -Literal $updateOSwithoutBuild), $updateOSwithoutBuild)

    $updateOSwithBuild = Join-Path $baseUpdatesDir ('Windows{0}.{1}-KB{2}-x{3}.msu' -f $global:thisOSVersionShort, $global:thisOSBuild, $kb, $bitVersion)
    DBG ('Update for this OS build version exists: {0} | {1}' -f (Test-Path -Literal $updateOSwithBuild), $updateOSwithBuild)

    $updateExists = $null
    if (Test-Path -Literal $updateOSwithBuild) {

      $updateExists = $updateOSwithBuild
    
    } elseif (Test-Path -Literal $updateOSwithoutBuild) {

      $updateExists = $updateOSwithoutBuild
    }

    DBG ('Valid update for this OS found: {0}' -f $updateExists)
    DBGIF ('Cannot find a valid update for this OS which is required: {0} | {1} | {2}' -f $kb, $updateOSwithoutBuild, $updateOSwithBuild) { (-not $mayNotExist) -and (Is-EmptyString $updateExists) }

    if (Is-ValidString $updateExists) {

      DBG ('Going to install the update found for this OS: {0}' -f $updateExists)

      ReEnable-WindowsUpdateIfNecessary

      Run-Process 'wusa' ('"{0}" /quiet /norestart' -f $updateExists)

      ReDisable-Updates
    }
  }
}


function global:ReDisable-SystemRestore ()
{
  #===============
  # It seams some installations enable System Restore again and create shadow copy
  # thus we need to disable it again
  DBG ("Disable System Restore (System Protection - SR) after installation again: {0}" -f (Parse-BoolSafe($vmConfig.commonVM.systemRestoreOff)))

  if (Parse-BoolSafe($vmConfig.commonVM.systemRestoreOff))
  {
    Disable-SystemRestore
  }
}


function global:ReDisable-Updates ()
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBG ("Disable automatic updates: {0}" -f (Parse-BoolSafe($vmConfig.commonVM.updateOff)))

  if (Parse-BoolSafe($vmConfig.commonVM.updateOff)) {

    Set-RegistryValue 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update' AUOptions 1 DWord

    if ($global:thisOSVersionNumber -ge 10) {

      # Note: if we wanted we would be able to set the Policy keys with some advanced values
      #       but I intentionally do not want to touch Policy keys which then conflict with GPO application
      DBG ('Disabling Windows Update on Windows 10')

      Take-RegOwnership 'SOFTWARE\Microsoft\WindowsUpdate\Ux'
      Set-RegPermissions 'SOFTWARE\Microsoft\WindowsUpdate\Ux' 'Administrators'
  
      Set-RegistryValue 'HKLM:\SOFTWARE\Microsoft\WindowsUpdate\Ux' IsConvergedUpdateStackEnabled 0 Dword
      Set-RegistryValue 'HKLM:\SOFTWARE\Microsoft\WindowsUpdate\Ux\Settings' UxOption 0 DWord
      Set-RegistryValue 'HKLM:\SOFTWARE\Microsoft\WindowsUpdate\Ux\Settings' DeferUpgrade 1 Dword

      DBG ('According to my testing, otherwise than disabling the service completelly does not help on Windows 10/2016')
      DBGSTART
      Stop-Service wuauserv
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND

      [WMI] $wuauserv = Get-WmiQuerySingleObject '.' 'SELECT * FROM Win32_Service WHERE Name = "wuauserv"'
      DBGIF $MyInvocation.MyCommand.Name { Is-Null $wuauserv }

      if (Is-NonNull $wuauserv) {

        DBG ('Disable the service: {0} | {1} | {2} | {3}' -f $wuauserv.Name, $wuauserv.PathName, $wuauserv.StartMode, $wuauserv.State)
        DBGSTART
        $wmiRs = $null
        $wmiRs = $wuauserv.ChangeStartMode('Disabled')
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
        DBGWMI $wmiRs
      }
    }
  }

 
  DBG ('Disable IE 10+ automatic upgrades: {0}' -f (Parse-BoolSafe $vmConfig.commonVM.ie10updateOff))
  if (Parse-BoolSafe $vmConfig.commonVM.ie10updateOff) {

    if ((Get-IEVersion) -ge 10) {

      Set-RegistryValue 'HKLM:\SOFTWARE\Microsoft\Internet Explorer\Main' EnableAutoUpgrade 0 Dword
    }
  }

  
  DBG ('Disable Win10+ diagnostics/telemetry upload: {0}' -f (Parse-BoolSafe $vmConfig.commonVM.telemetryOff))
  $telemetryUploadSvc = Get-WmiQuerySingleObject '.' 'SELECT * FROM Win32_Service WHERE Name = "DiagTrack"'
  DBG ('Telemetry service state: {0} | {1} | {2}' -f $telemetryUploadSvc.StartMode, $telemetryUploadSvc.State, $telemetryUploadSvc.DisplayName)
  DBGIF $MyInvocation.MyCommand.Name { ($global:thisOSVersionNumber -ge 10) -and (Is-Null $telemetryUploadSvc) }

  if ((Parse-BoolSafe $vmConfig.commonVM.telemetryOff) -and (Is-NonNull $telemetryUploadSvc)) {

    DBG ('Disable the telemetry DiagTrack service')
    DBGSTART
    $wmiRs = $null
    $wmiRs = $telemetryUploadSvc.ChangeStartMode('Disabled')
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBGWMI $wmiRs
  }
}


function global:ReEnable-WindowsUpdateIfNecessary ()
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { $global:thisOSVersionNumber -lt 10 }

  $wuauserv = Get-WmiQuerySingleObject '.' 'SELECT * FROM Win32_Service WHERE Name = "wuauserv"'
  DBG ('Windows Update currently running: {0} | mode = {1}' -f $wuauserv.Started, $wuauserv.StartMode)

  if (-not $wuauserv.Started) {

    if ($wuauserv.StartMode -eq 'Disabled') {

      DBG ('Changing the Windows Update start mode to Manual')
      DBGSTART
      $wmiRs = $null
      $wmiRs = $wuauserv.ChangeStartMode('Manual')
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
      DBGWMI $wmiRs
    }

    DBG ('Starting the Windows Update service')
    DBGSTART
    Start-Service wuauserv
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
  }
}


function global:Do-DSACLS ([string] $dsacls, [System.Xml.XmlElement] $baseElementForMachineRef, [string] $domainDN)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $dsacls }

  if (Is-ValidString $dsacls) {

    $params = Split-MultiValue $dsacls
    DBGIF $MyInvocation.MyCommand.Name { $params.Count -ne 5 }


  # ---------------------
    $rawDN = Strip-ValueFlags $params[0]
    $rawDNprm = Get-ValueFlags $params[0]
    DBGIF $MyInvocation.MyCommand.Name { (Is-ValidString $rawDN) -and ($rawDN -notmatch (Get-NormalDNMatch $true)) }
    DBG ('DN split to params as: raw = {0} | prm = {1}' -f $rawDN, $rawDNprm)

    if ((Is-ValidString $rawDN) -and (Is-EmptyString $rawDNprm)) {

      $trgDN = $rawDN
    
    } else {

      DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $rawDNprm }

      if ($rawDNprm -eq 'L') {

        DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $rawDN }
        $resolveLoginRef = $rawDN
        $trgDN = Get-SecurityPrincipalDNfromLogin $resolveLoginRef
        
      } elseif ($rawDNprm -eq 'M') {

        DBGIF $MyInvocation.MyCommand.Name { Is-ValidString $rawDN }
        DBGIF $MyInvocation.MyCommand.Name { Is-Null $baseElementForMachineRef }
        $resolveLoginRef = Get-MachineSAMLogin $baseElementForMachineRef
        $trgDN = Get-SecurityPrincipalDNfromLogin $resolveLoginRef
    
      } elseif ($rawDNprm -eq 'D') {

        DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $domainDN }
        $trgDN = $domainDN
      
      } elseif ($rawDNprm -eq 'C') {

        DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $domainDN }
        $trgDN = 'CN=Configuration,{0}' -f $domainDN
      
      } elseif ($rawDNprm -eq 'S') {

        DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $domainDN }
        $trgDN = '{0},{1}' -f $rawDN, $domainDN
      
      } else {

        DBGIF ('Invalid target DN type spec: {0}' -f $rawDNprm) { $true }
      }
    }
 
    DBG ('Target DN determined as: {0}' -f $trgDN)   
    DBGIF ('Weird target DN syntax: {0}' -f $trgDN) { $trgDN -notmatch (Get-NormalDNMatch $true) }
  # ---------------------


  # ---------------------
    $rawInheritance = $params[1]
    $trgInheritance = ''
    if ((Is-ValidString $rawInheritance) -and ($rawInheritance -ne $global:emptyValueMarker)) {

      $trgInheritance = '/I:{0}' -f $rawInheritance
    }
  # ---------------------


  # ---------------------
    $trgOperation = $params[2]
    DBGIF $MyInvocation.MyCommand.Name { $trgOperation -notlike '[GRD]' }
  # ---------------------


  # ---------------------
    $rawIdentity = Strip-ValueFlags $params[3]
    $rawIdentityPrm = Get-ValueFlags $params[3]
    DBGIF $MyInvocation.MyCommand.Name { (Is-ValidString $rawIdentityPrm) -and (Is-ValidString $rawIdentity) }

    $trgIdentity = ''
    if (Is-ValidString $rawIdentity) {

      $trgIdentity = $rawIdentity
    
    } else {

      DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $rawIdentityPrm }

      if ($rawIdentityPrm -eq 'M') {

        DBGIF $MyInvocation.MyCommand.Name { Is-Null $baseElementForMachineRef }
        $trgIdentity = Get-MachineSAMLogin $baseElementForMachineRef
      }
    }
    
    DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $trgIdentity }
  # ---------------------


  # ---------------------
    $trgWhat = $params[4]
    DBGIF ('Weird permission spec: {0}' -f $trgWhat) { $trgWhat -notmatch '\A[a-z][a-z]+;([a-z][a-z \-]*\Z)|([a-z \-]*;[a-z][a-z \-]+\Z)' }
  # ---------------------


    Assert-AccountExists $trgIdentity 'Call DSACLS for identity'

    Run-Process 'DSACLS' ('"{0}" {1} /{2} "{3}:{4}"' -f $trgDN, $trgInheritance, $trgOperation, $trgIdentity, $trgWhat)
  }
}


function global:Assert-CertificatesInStores ([string] $userOrMachine = 'CurrentUser')
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $userOrMachine }
  DBGIF $MyInvocation.MyCommand.Name { ($userOrMachine -ne 'CurrentUser') -and ($userOrMachine -ne 'LocalMachine') }

  [bool] $isMachine = $userOrMachine -ne 'CurrentUser'

  if (Is-ValidString $userOrMachine) {

    $rootStores = dir cert:\$userOrMachine
    DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $rootStores) -lt 5 } # Note: just some small number greater than 1 :-)
    DBG ('Found the following stores: {0} | {1}' -f (Get-CountSafe $rootStores), (($rootStores | Select -Expand Name) -join ','))

    if ((Get-CountSafe $rootStores) -gt 0) {

      [Collections.ArrayList] $checkedThumbprints = @()
      foreach ($oneStore in $rootStores) { 
  
        $storePath = Join-Path Cert:\$userOrMachine $oneStore.Name
        DBG ('Get certificates in the store: {0}' -f $storePath)
        DBGIF $MyInvocation.MyCommand.Name { -not $oneStore.PsIsContainer }
        DBGIF $MyInvocation.MyCommand.Name { $oneStore -isnot [System.Security.Cryptography.X509Certificates.X509store] }

        DBGSTART
        $oneStoreCertificates = $null
        $oneStoreCertificates = dir $storePath -Recurse
        DBGER ('Cannot enumerate certificates in: {0}' -f $storePath) $error
        DBGEND
        DBG ('Found certificates in store: #{0} | {1}' -f (Get-CountSafe $oneStoreCertificates), $storePath)

        if ((Get-CountSafe $oneStoreCertificates) -gt 0) {

          $oneStoreCertificates | ? { $_ -isnot [System.Security.Cryptography.X509Certificates.X509store] } | ? { -not (Contains-Safe $checkedThumbprints $_.Thumbprint) } | % { 
          
            Assert-CertificatePrivateKey $_ $isMachine
            [void] $checkedThumbprints.Add($_.Thumbprint)
            
            } | Out-Null
        }
      }
  

      dir cert:\$userOrMachine\My | % { 
  
        $oneCert = $_
        $oneCertFile = Get-DataFileApp ('exported-{0}-certificate-{1}' -f $userOrMachine, $oneCert.Thumbprint) $null '.cer'
        Save-Certificate $oneCert $oneCertFile
        Run-Process certutil ('-urlfetch -verify "{0}"' -f $oneCertFile)
      }
    }
  }
}


function global:Get-NatRulesForInstance ([string] $instanceName)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $instanceName }
  
  [System.Collections.ArrayList] $natRuleList = @()


  $natRules = $xmlConfig.SelectNodes('./VMs/MACHINE[vm/@do="true"]/*/nat[translate(@instance,"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz")="{0}"]' -f $instanceName.ToLower())
  DBG ('NAT rules: {0}' -f (Get-CountSafe $natRules))

  $duplicateNATRules = Find-DuplicatesInSortedList ($natRules | Sort-Object publicPort,publicIP) publicPort,publicIP
  DBGIF ('Severe NAT configuration error - duplicate NAT rules: {0}' -f ($duplicateNATRules | Out-String)) { (Get-CountSafe ($duplicateNATRules)) -gt 0 }

  foreach ($oneNATRule in $natRules) {

    if (Contains-Safe $duplicateNATRules $oneNATRule publicPort,publicIP) {

      DBG ('Skipping NAT rule due to duplicacy: if = {0} | proto = {1} | publicIP = {2} | publicPort = {3} | privateIP = {4} | privatePort = {5}' -f $oneNATRule.if, $oneNATRule.protocol, $oneNATRule.publicIP, $oneNATRule.publicPort, $oneNATRule.privateIP, $oneNATRule.privatePort)

    } else {

      DBG ('Going to process NAT rule: if = {0} | proto = {1} | publicIP = {2} | publicPort = {3} | privateIP = {4} | privatePort = {5}' -f $oneNATRule.if, $oneNATRule.protocol, $oneNATRule.publicIP, $oneNATRule.publicPort, $oneNATRule.privateIP, $oneNATRule.privatePort)
    

      [string[]] $natRuleIfs = @()
      if (Is-EmptyString $oneNATRule.if) {

        DBG ('Will apply the NAT rule to all public NAT interfaces')
        
        # Note: in case of TMG publishing these NAT rules there will be no
        #       natType interfaces at all

        $natRuleIfs = (Obtain-ListMembers (Get-NICsListFromXmlConfig) @{ 'natType' = 'E' }) | Select-Object -Expand name

      } else {
 
        $natRuleIfs = Split-MultiValue $oneNATRule.if
      }

      DBG ('NAT rule interfaces found: #{0} | {1}' -f (Get-CountSafe $natRuleIfs), ($natRuleIfs -join ','))


      $natRulePrivateIP = $oneNATRule.privateIP


      if (Is-EmptyString $oneNATRule.publicIP) {

        DBG ('Default to 0.0.0.0 for public IP')
        $natRulePublicIP = '0.0.0.0'

      } else {

        $natRulePublicIP = $oneNATRule.publicIP
      }


      if (Is-EmptyString $oneNATRule.privatePort) {

        DBG ('Default privatePort to be the same as publicPort: {0}' -f $oneNATRule.publicPort)
        $natRulePrivatePort = $oneNATRule.publicPort

      } else {

        $natRulePrivatePort = $oneNATRule.privatePort
      }


      if ($natRulePublicIP -notmatch $global:rxIPv4) {

        DBG ('Public IP is not an IP address, translate its DNS name to an IP address: {0}' -f $natRulePublicIP)
        $natRulePublicIP = (Resolve-DNSNameWithConfig $natRulePublicIP $null).ipAddress
      }


      if ($natRulePrivateIP -notmatch $global:rxIPv4) {

        if ($natRulePrivateIP -eq $global:emptyValueMarker) {

          DBG ('Private IP is not an IP address, translate its DNS name to an IP address for the current MACHINE: {0}' -f $natRulePrivateIP)
          $natRulePrivateIP = (Resolve-DNSNameWithConfig '-' $oneNATRule).ipAddress

        } else {

          DBG ('Private IP is not an IP address, translate its DNS name to an IP address for the DNS NAME: {0}' -f $natRulePrivateIP)
          $natRulePrivateIP = (Resolve-DNSNameWithConfig $natRulePrivateIP $null).ipAddress
        }
      }
     

      foreach ($oneNATRuleIF in $natRuleIfs) { 
      
        $newNatRule = New-Object PSObject

        Add-Member -Input $newNatRule -MemberType NoteProperty -Name if -Value $oneNATRuleIF
        Add-Member -Input $newNatRule -MemberType NoteProperty -Name protocol -Value $oneNATRule.protocol.ToUpper()
        Add-Member -Input $newNatRule -MemberType NoteProperty -Name publicIP -Value $natRulePublicIP
        Add-Member -Input $newNatRule -MemberType NoteProperty -Name publicPort -Value $oneNATRule.publicPort
        Add-Member -Input $newNatRule -MemberType NoteProperty -Name privateIP -Value $natRulePrivateIP
        Add-Member -Input $newNatRule -MemberType NoteProperty -Name privatePort -Value $natRulePrivatePort

        DBG ('Define new NAT rule: if = {0} | proto = {1} | publicIP = {2} | publicPort = {3} | privateIP = {4} | privatePort = {5}' -f $newNatRule.if, $newNatRule.protocol, $newNatRule.publicIP, $newNatRule.publicPort, $newNatRule.privateIP, $newNatRule.privatePort)
        [void] $natRuleList.Add($newNatRule)
      }
    }
  }


  DBG ("Returning NAT rules: {0} | -->`r`n{1}" -f $natRuleList.Count, ($natRuleList | ft -Auto | Out-String))
  return ,$natRuleList
}


function global:Verify-SvchostServiceRegistrations ([bool] $fix)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  [bool] $anyCorrections = $false

  $services = Get-WmiQueryArray '.' ('SELECT * FROM Win32_Service WHERE PathName LIKE "%{0}%"' -f "$env:SystemDrive\\Windows\\System32\\svchost.exe")
  DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $services) -lt 10 }

  if ((Get-CountSafe $services) -gt 0) {

    foreach ($oneService in $services) {
    
      $rxContext = '.+ -k ([a-zA-Z][a-zA-Z0-9]+)(?:| -p)\Z'
      DBGIF ('Weird svchost PathName: {0} | {1}' -f $oneService.Name, $oneService.PathName) { $oneService.PathName -notmatch $rxContext }
      [string] $context = ''
      [string] $context = [regex]::Match($oneService.PathName, $rxContext).Groups[1]
      DBG ('One service context: {0} | {1}' -f $oneService.Name, $context)
      DBGIF ('Empty svchost context: {0}' -f $oneService.Name) { Is-EmptyString $context }

      if (Is-ValidString $context) {

        $svchostKey = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Svchost'

        DBG ('Get the context list from registry: {0} | {1}' -f $context, $svchostKey)
        DBGSTART
        [string[]] $contextVal = (Get-ItemProperty -Path $svchostKey -Name $context).$context
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
        
        DBG ('Context value contains the following services: {0} | {1}' -f $context, ($contextVal -join ','))
        DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $contextVal) -lt 1 }

        # Note: since win10/win2016 there are services with names such as PimIndexMaintenanceSvc_2d206 or CDPUserSvc_2d206
        #       which are registered with the pure name without the _2d206

        [string] $serviceName = $oneService.Name
        [string] $serviceNameNormal = $serviceName

        if (($serviceNameNormal -match '\A.+_[0-9a-zA-Z]+\Z') -and ($global:thisOSVersionNumber -ge 10)) {

          $serviceNameNormal = $serviceNameNormal.SubString(0, $serviceNameNormal.IndexOf('_'))
        }

        DBGIF ('SVCHOST service is missing registration: {0} | {1} | {2}' -f $oneService.Name, $oneService.DisplayName, $context) { (-not (Contains-Safe $contextVal $serviceName)) -and (-not (Contains-Safe $contextVal $serviceNameNormal)) }

        if ($fix -and (-not (Contains-Safe $contextVal $serviceName)) -and (-not (Contains-Safe $contextVal $serviceNameNormal))) {

          $anyCorrections = $true

          [string[]] $newContextVal = $contextVal + $oneService.Name
          DBG ('Going to fix the SVCHOST registration: {0} | context = {1} | newList = {2}' -f $oneService.Name, $context, ($newContextVal -join ','))
          DBGSTART
          Set-ItemProperty -Path $svchostKey -Name $context -Value $newContextVal -Type MultiString
          DBGER $MyInvocation.MyCommand.Name $error
          DBGEND
        }
      }
    }
  }

  DBGIF ('Fixed at least one SVCHOST registration. Must restart') { $anyCorrections }
  return $anyCorrections
}


function global:Create-Partition ($partition)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { Is-Null $partition }

  if (Is-NonNull $partition)
  {
    DBG ('Going to create partition: diskSize = {0} | diskId = {1} | {2} | {3} | {4} | {5}' -f $partition.diskSize, $partition.diskId, $partition.letter, $partition.size, $partition.init, $partition.fs)

    [string] $diskId = $partition.diskId

    if (Is-EmptyString $diskId) {

      DBG ('Disk ID not specified, going to find the best disk')

      if (Is-ValidString $partition.diskSize) {

        DBG ('Must find the disk: size = {0}' -f $partition.diskSize)
        [wmi] $bestDisk = $null
        $bestDisk = 
          gwmi Win32_DiskDrive | 
             % { 
                 $percentRequiredSizeDelta = [Math]::Abs((1.0 - (1MB * ([long] $partition.diskSize) / $_.Size)))
                 Add-Member -Input $_ -MemberType NoteProperty -Name percentRequiredSizeDelta -Value $percentRequiredSizeDelta
                 Write-Output $_

               } | Sort percentRequiredSizeDelta | Select -First 1

        DBG ('Best disk determined as: idx = {0} | size = {1} | id = {2} | % = {3:N4}' -f $bestDisk.Index, $bestDisk.Size, $bestDisk.DeviceId, $bestDisk.percentRequiredSizeDelta)
        DBGIF $MyInvocation.MyCommand.Name { Is-Null $bestDisk }

        # Note: for example 682MB disk has actual size of 658022400
        DBGIF $MyInvocation.MyCommand.Name { $bestDisk.percentRequiredSizeDelta -gt 0.1 }

        if ($bestDisk.percentRequiredSizeDelta -lt 0.1) {

          $diskId = $bestDisk.Index
        }
      }
    }

    DBG ('Open the selected disk: id = {0}' -f $diskId)
    [wmi] $diskFound = Get-WmiQuerySingleObject '.' ('SELECT * FROM Win32_DiskDrive WHERE Index = "{0}"' -f $diskId)

    if (Is-NonNull $diskFound) {

      DBG ('Will use the disk: idx = {0} | size = {1} | id = {2} | sig = {3} | sector = {4} | partNo = {5}' -f $diskFound.Index, $diskFound.Size, $diskFound.DeviceId, $diskFound.Signature, $diskFound.BytesPerSector, $diskFound.Partitions)

      # Note: the .Signature field is either some number for MBR
      #       or it is explicitly NULL for GPT format
      #       or it is 0 for RAW format
      if (($partition.init -eq 'mbr') -and ($diskFound.Signature -eq 0)) {

        DBG ('Making the disk MBR')
        $makeMbr = @"

          SELECT DISK $($diskFound.Index)
          ONLINE DISK NOERR
          ATTRIBUTES DISK CLEAR READONLY
          CONVERT MBR
"@

        [int[]] $ignoreExitCodes = @()

        if ($global:thisOSVersionNumber -eq 6.0) {

          # Note: Windows Vista's CONVERT MBR errors out on empty disk with error 5 - the disk is not GPT formated
          $ignoreExitCodes += 5
        }

        Call-DiskPart $makeMbr 'diskpart-make-disk-mbr' -ignoreExitCodes $ignoreExitCodes

      } elseif (($partition.init -eq 'gpt') -and ($diskFound.Signature -eq 0)) {

        DBG ('Making the disk GPT')
        $makeMbr = @"

          SELECT DISK $($diskFound.Index)
          ONLINE DISK NOERR
          ATTRIBUTES DISK CLEAR READONLY
          CONVERT GPT
"@

        Call-DiskPart $makeMbr 'diskpart-make-disk-gpt'

      } else {

        DBG ('The disk is already initialized to GPT or MBR')
      }


      DBG ('Get the disk partitions')
      [object[]] $existingPartitions = (Get-WmiQueryArray '.' ('SELECT * FROM Win32_DiskPartition WHERE DiskIndex = "{0}"' -f $diskFound.Index)) | Sort StartingOffset
      
      [long] $longestFreeSpace = 0

      if ($existingPartitions.Length -gt 0) {

        for ([int] $i = 0; $i -le ($existingPartitions.Length - 2); $i ++) {

          [long] $oneExistingPartitionEndOffset = $existingPartitions[$i].StartingOffset + $existingPartitions[$i].Size
          DBG ('One partition: at = {0} | size = {1} | endOffset = {2}' -f $existingPartitions[$i].StartingOffset, $existingPartitions[$i].Size, $oneExistingPartitionEndOffset)
          $longestFreeSpace = [Math]::Max($longestFreeSpace, ($existingPartitions[$i + 1].StartingOffset - $oneExistingPartitionEndOffset))
          DBG ('Longest free space yet: {0:N0} MB' -f ($longestFreeSpace / 1MB))
        }

        $i = $existingPartitions.Length - 1
        [long] $lastPartitionEndOffset = $existingPartitions[$i].StartingOffset + $existingPartitions[$i].Size
        DBG ('Last partition: at = {0} | size = {1} | endOffset = {2}' -f $existingPartitions[$i].StartingOffset, $existingPartitions[$i].Size, $lastPartitionEndOffset)
        $longestFreeSpace = [Math]::Max($longestFreeSpace, ($diskFound.Size - $lastPartitionEndOffset))
        DBG ('Longest free space yet: {0:N0} MB' -f ($longestFreeSpace / 1MB))
      
      } else {

        $longestFreeSpace = $diskFound.Size
      }

      DBG ('Longest free space determined as: {0:N0} MB' -f ($longestFreeSpace / 1MB))



      DBG ('Letter specified for partition: {0}' -f $partition.letter)
      [string] $assignLetterCommand = 'ASSIGN'

      if (Is-ValidString $partition.letter) {

        $assignLetterCommand += ' LETTER={0}' -f $partition.letter[0]
      }



      [long] $newPartitionSize = -1

      if (Is-EmptyString $partition.size) {

        DBG ('Create partition full size')

      } elseif ($partition.size -notlike '?*%') {

        $newPartitionSize = $partition.size
        DBG ('Create partition explicit size: {0}' -f $newPartitionSize)
        DBGIF $MyInvocation.MyCommand.Name { $newPartitionSize -gt $longestFreeSpace }

      } else {

        [int] $newPartitionSizePercent = [regex]::Match($partition.size, '(.+)\%').Groups[1].Value
        DBGIF $MyInvocation.MyCommand.Name { $newPartitionSizePercent -gt 99 }
        $newPartitionSize = $longestFreeSpace * $newPartitionSizePercent / 100 / 1MB
        DBG ('Create partition explicit size: {0}' -f $partition.size)
      }


      [string] $createPartitionCommand = 'CREATE PARTITION PRIMARY'

      if ($newPartitionSize -ne -1) {

        $createPartitionCommand += ' SIZE={0}' -f $newPartitionSize
      }


      [string] $formatCmd = ''
      if (Is-ValidString ($partition.fs)) {

        DBG ('Will format with: {0}' -f $partition.fs)
        $formatCmd = "FORMAT FS=$($partition.fs)"
      }


      $partitionExplicitSizeScr = @"
        SELECT DISK $($diskFound.Index)
        $createPartitionCommand
        $assignLetterCommand
        $formatCmd
"@

      Call-DiskPart $partitionExplicitSizeScr ('diskpart-create-new-partition-{0:X6}' -f (Get-Random -Minimum 0 -Maximum 0xFFFFFF))
    }
  }
}


function global:Deploy-WimUEFI ([string] $wim, [int] $image, [string] $vhdOrVHDX) 
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $wim }
  DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path -Literal $wim) }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $vhdOrVHDX }
  DBGIF $MyInvocation.MyCommand.Name { Test-Path -Literal $vhdOrVHDX }

  if ((Is-EmptyString $wim) -or (Is-EmptyString $vhdOrVHDX) -or (Test-Path $vhdOrVHDX) -or (-not (Test-Path $wim))) {

    return
  }

  $mountTemp = Get-DataFileApp ('{0}-{1}' -f ([IO.Path]::GetFileNameWithoutExtension((Split-Path -Leaf $vhdOrVHDX))), ([DateTime]::Now).ToString('yyyy-MM-dd-HH-mm-ss')) -noExtension $true -doNotPrefixWithOutFile $true
  DBG ('Create VHD mount temp: {0}' -f $mountTemp)
  DBGSTART
  New-Item $mountTemp -ItemType Directory | Out-Null
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND

  [string[]] $assignedDeviceIds = (Get-WmiQueryArray '.' 'SELECT * FROM Win32_LogicalDisk') | ? { Is-ValidString $_.DeviceId } | Select -Expand DeviceId
  DBG ('Already used drive letters: {0}' -f ($assignedDeviceIds -join ','))
  DBGIF $MyInvocation.MyCommand.Name { $assignedDeviceIds.Length -gt 24 }

  [string[]] $freeLetters = @()

  for ($i = ([System.Text.Encoding]::ASCII.GetBytes('Z'))[0]; $i -ge ([System.Text.Encoding]::ASCII.GetBytes('A'))[0]; $i--) {

    [string] $oneLetter = '{0}:' -f ([char] $i)
    if (-not (Contains-Safe $assignedDeviceIds $oneLetter)) {

      $freeLetters += $oneLetter
    }

    if ($freeLetters.Length -ge 2) {

      break
    }
  }

  [string] $partitionEFI = $freeLetters[0]
  [string] $partitionRecovery = $freeLetters[1]
  DBG ('Will use free letters: EFI = {0} | Recovery = {1} | mount = {2}' -f $partitionEFI, $partitionRecovery, $mountTemp)

  $imagesOk = Run-Process ('{0}\ForeignBinaries\DISM\DISM10-64bit\dism.exe' -f $global:libCommonParentDir) ('/Get-ImageInfo /ImageFile:"{0}"' -f $wim) -returnExitCode $true
  if ($imagesOk -ne 0) { return }

  if ($image -lt 1) {

    $image = Ask-UserForInt -query 'Specify the number of image to apply'
  }

  $imageOk = Run-Process ('{0}\ForeignBinaries\DISM\DISM10-64bit\dism.exe' -f $global:libCommonParentDir) ('/Get-ImageInfo /ImageFile:"{0}" /Index:{1}' -f $wim, $image) -returnExitCode $true
  if ($imageOk -ne 0) { return }

  $diskPartScr = @"
    CREATE VDISK FILE="$vhdOrVHDX" MAXIMUM=88064 TYPE=EXPANDABLE
    SELECT VDISK FILE="$vhdOrVHDX"
    ATTACH VDISK 
    CONVERT GPT
    SELECT PARTITION 1
    DELETE PARTITION NOERR OVERRIDE
    CREATE PARTITION MSR SIZE=16
    CREATE PARTITION EFI SIZE=128
    FORMAT FS=FAT32 LABEL="EFISYSTEM" QUICK
    ASSIGN LETTER=$partitionEFI
    CREATE PARTITION PRIMARY SIZE=640 ID=de94bba4-06d1-4d40-a16a-bfd50179d6ac
    FORMAT FS=NTFS LABEL="RECOVERY" QUICK
    ASSIGN LETTER=$partitionRecovery
    CREATE PARTITION PRIMARY
    FORMAT FS=NTFS LABEL="WINDOWS" QUICK
    ATTRIBUTES VOLUME CLEAR NODEFAULTDRIVELETTER
    ASSIGN MOUNT="$mountTemp"
"@

  Call-DiskPart $diskPartScr ('{0}-create-attach-VHD' -f (Split-Path -Leaf $vhdOrVHDX))

  $appliedOk = Run-Process ('{0}\ForeignBinaries\DISM\DISM10-64bit\dism.exe' -f $global:libCommonParentDir) ('/Apply-Image /ImageFile:"{0}" /Index:{1} /ApplyDir:"{2}"' -f $wim, $image, $mountTemp) -returnExitCode $true
  if ($appliedOk -ne 0) { return }

  Run-Process ('{0}\Windows\System32\bcdboot' -f $mountTemp) ('{0}\Windows /s {1} /f UEFI' -f $mountTemp, $partitionEFI)
  
  DBG ('Create the WindowsRE directory: {0}' -f $partitionRecovery)
  DBGSTART
  New-Item -ItemType Directory ('{0}\Recovery\WindowsRE' -f $partitionRecovery) | Out-Null
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND

  DBG ('Copy the WinRE into the recovery partition: {0}' -f $mountTemp, $partitionRecovery)
  DBGSTART
  Copy-Item ('{0}\Windows\System32\Recovery\Winre.wim' -f $mountTemp) ('{0}\Recovery\WindowsRE\Winre.wim' -f $partitionRecovery) -Force
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND

  # Note: 0xC0000135 = WinRE already enabled
  Run-Process ('{0}\Windows\System32\reagentc' -f $mountTemp) ('/SetReImage /Path {0}\Recovery\WindowsRE /Target {1}\Windows' -f $partitionRecovery, $mountTemp) -ignoreExitCodes @(0xC0000135)

  $diskPartScr = @"
    SELECT VDISK FILE="$vhdOrVHDX"
	SELECT PARTITION 4
	REMOVE MOUNT="$mountTemp"
	SELECT PARTITION 3
	REMOVE LETTER=$partitionRecovery
	SELECT PARTITION 2
	REMOVE LETTER=$partitionEFI
	DETACH VDISK
"@

  Call-DiskPart $diskPartScr ('{0}-dettach-VHD' -f (Split-Path -Leaf $vhdOrVHDX))

  DBG ('Finished')
}


function global:Import-BitColdKit ()
{
  [string] $bitColdKit = Join-Path $global:libCommonParentDir 'BitColdKit\bitcoldkit.psm1'
  if (-not (Test-Path $bitColdKit)) {

    $bitColdKit = Join-Path $global:libCommonParentDir 'bitcoldkit.psm1'
  }

  DBG ('Import BitColdKit module first: {0}' -f $bitColdKit)
  DBGSTART
  Import-Module $bitColdKit
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
}


function global:UnlockAndVerify-BitLockerRecoveryDecryption ([string] $recoveryFiles, [string] $bekLocation = 'A:')
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))


  Import-BitColdKit
 

  if (-not (Test-Path $recoveryFiles)) {

    DBG ('The recovery files folder does not exist, create: {0}' -f $recoveryFiles)
    [void] (mkdir $recoveryFiles)
  }


  DBG ('Get all volume parameters')
  DBGSTART
  $recoveryInfos = BitColdKit-ExportDiskRecoveryInfo -folderPath $recoveryFiles -includeLockedVolumes
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
  DBG ('Found volume recovery infos: {0}' -f (Get-CountSafe $recoveryInfos))
  DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $recoveryInfos) -lt 1 }

  if ((Get-CountSafe $recoveryInfos) -gt 0) {

    DBG ('Get all locked volumes to unlock')
    $lockedBEKs = $recoveryInfos | ? { ($_.lockStatus -eq 'locked') -and ($_.type -eq 'external key file') }
    DBG ('Locked volumes found: {0}' -f (Get-CountSafe $lockedBEKs))

    if ((Get-CountSafe $lockedBEKs) -gt 0) {
 
      foreach ($oneLockedBEK in $lockedBEKs) {

        DBG ('One locked volume with BEK: {0} | {1} | {2}' -f $oneLockedBEK.volume, $oneLockedBEK.id, $oneLockedBEK.volumeDeviceId)
        [string] $bekFile = Join-Path $bekLocation ('{0}.bek' -f $oneLockedBEK.id)
        DBG ('One BEK file to unlock the volume: {0}' -f $bekFile)
        DBGIF ('The BEK file does not exist: {0}' -f $bekFile) { -not (Test-Path -Literal $bekFile) }

        [wmi] $bekVolume = $null
        $bekVolume = Get-WmiQuerySingleObject '.' ('SELECT * FROM Win32_EncryptableVolume WHERE DeviceId = "{0}"' -f $oneLockedBEK.volumeDeviceId.Replace('\', '\\')) -namespace 'root\cimv2\Security\MicrosoftVolumeEncryption'
        DBGIF $MyInvocation.MyCommand.Name { Is-Null $bekVolume }
      
        if (Is-NonNull $bekVolume) {

          DBG ('Get the external key from file')
          DBGSTART
          [byte[]] $bek = $bekVolume.GetExternalKeyFromFile($bekFile).ExternalKey
          DBGER $MyInvocation.MyCommand.Name $error
          DBGEND
          DBG ('External key loaded: {0} | {1}' -f ([BitConverter]::ToString($bek)), $bek.Length)
          DBGIF $MyInvocation.MyCommand.Name { $bek.Length -ne 32 }

          DBG ('Unlock the volume')
          DBGSTART
          $wmiRs = $bekVolume.UnlockWithExternalKey($bek)
          DBGER $MyInvocation.MyCommand.Name $error
          DBGEND
          DBGWMI $wmiRs

          DBGIF $MyInvocation.MyCommand.Name { Is-Null (Get-Item $oneLockedBEK.volume) }
        }
      }
    }
  }


  #
  #
  #

  DBG ('Add password48 decryptor for all volumes - explicit pwd')
  DBGSTART
  $protectorsCreated = $null
  $protectorsCreated = BitColdKit-Create48PasswordProtectorForAllVolumes -friendlyName 'SevecekPwd48' -password48 '285681-285714-285505-127897-313874-274340-296956-285604'
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
  DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $protectorsCreated) -lt 1 }

  DBG ('Add password48 decryptor for all volumes - random pwd')
  DBGSTART
  $protectorsCreated = $null
  $protectorsCreated = BitColdKit-Create48PasswordProtectorForAllVolumes -friendlyName 'SevecekRandomPwd48'
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
  DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $protectorsCreated) -lt 1 }

  if ($global:thisOSVersionNumber -ge 6.1) {

    DBG ('Add password decryptor for all volumes')
    DBGSTART
    $protectorsCreated = $null
    $protectorsCreated = BitColdKit-CreatePasswordProtectorForAllVolumes -friendlyName 'SevecekPwd' -password 'Pa$$w0rd'
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $protectorsCreated) -lt 1 }
  }

  DBG ('Add password decryptor for all volumes - random BEK')
  DBGSTART
  $protectorsCreated = $null
  $protectorsCreated = BitColdKit-CreateExternalKeyProtectorForAllVolumes -friendlyName 'SevecekRandomBEK' -key32Generate
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND

  DBG ('Add password decryptor for all volumes - explicit BEK')
  DBGSTART
  $protectorsCreated = $null
  $protectorsCreated = BitColdKit-CreateExternalKeyProtectorForAllVolumes -friendlyName 'SevecekBEK' -key32HexString '00-01-02-03-04-05-06-07-08-09-0A-0B-0C-0D-0E-0F-00-10-20-30-40-50-60-70-80-90-A0-B0-C0-D0-E0-F0'
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND

  #
  #
  #

  DBG ('Get all volume parameters again')
  DBGSTART
  $recoveryInfosAgain = BitColdKit-ExportDiskRecoveryInfo -folderPath $recoveryFiles -includeLockedVolumes
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
  DBG ('Found volumes: {0}' -f (Get-CountSafe $recoveryInfosAgain))
  DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $recoveryInfosAgain) -lt 1 }

  DBG ('Get all locked volumes to check')
  $lockedVolumes = $recoveryInfosAgain | ? { ($_.lockStatus -eq 'locked') }
  DBGIF ('Still locked volumes: {0}' -f ($lockedVolumes | fl * | Out-String)) { (Get-CountSafe $lockedVolumes) -gt 0 }


  $keyPackages = $recoveryInfosAgain | ? { Is-ValidString $_.keyPackage }
  DBG ('Going to verify key package decryption for key packages: {0}' -f (Get-CountSafe $keyPackages))
  DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $keyPackages) -lt 1 }


  [string] $recoveryFilesCSV = Join-Path $recoveryFiles ('{0}.{1}-{2:X8}.csv' -f $global:thisComputerHost, $global:thisComputerDomain, (Get-Random -Minimum 0 -Maximum ([int]::MaxValue)))
  DBG ('First save the key package information to the output folder as CSV: {0}' -f $recoveryFilesCSV)
  $keyPackages | Export-Csv -Path $recoveryFilesCSV -Encoding Unicode -Delimiter "`t" -Force -NoTypeInformation

  
  [Collections.ArrayList] $decryptions = @()

  if (((Get-CountSafe $keyPackages) -gt 0) -and ($global:thisCLRVersion -ge 4)) {

    DBG ('We really can verify decryption on NETFX 4+')
    
    foreach ($oneKeyPackage in $keyPackages) {

      DBG ('Checking decryption of a key package: {0} | {1}' -f $oneKeyPackage.keyPackage, $oneKeyPackage.info)

      if ($oneKeyPackage.type -eq 'external key file') {

        DBG ('Decrypt using BEK')
        DBGSTART
        [void] $decryptions.Add((BitColdKit-DecryptExternalKeyPackage -keyPackage $oneKeyPackage.keyPackage -bekFile $oneKeyPackage.info))
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND

      } elseif ($oneKeyPackage.type -eq 'numerical password') {

        DBG ('Decrypt using numerical password')
        DBGSTART
        [void] $decryptions.Add((BitColdKit-Decrypt48PasswordKeyPackage -keyPackage $oneKeyPackage.keyPackage -password48 $oneKeyPackage.info))
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND

      } else {

        DBGIF ('Unsupported decyptor: {0}' -f $oneKeyPackage.type) { $true }
      }
    }

    DBGIF $MyInvocation.MyCommand.Name { $decryptions.Count -lt 1 }

    [string] $decryptionsCSV = Get-DataFileApp 'BitColdKit-verified-decryptions' $null '.csv'
    DBG ('Save the decryption results in CSV: {0}' -f $decryptionsCSV)
    $decryptions | Export-Csv -Path $decryptionsCSV -Encoding Unicode -Delimiter "`t" -Force -NoTypeInformation
  }


  #
  #
  #

  DBG ('Should we verify exporting recovery packages from AD: {0}' -f (Is-CurrentUser -domainAdmins $true))
  
  if (Is-CurrentUser -domainAdmins $true) {

    [string] $adExportPath = Join-Path $recoveryFiles ('AD-Export-{0}.{1}' -f $global:thisComputerHost, $global:thisComputerDomain)
    DBG ('Will export AD recovery info: {0}' -f $adExportPath)

    if (-not (Test-Path $adExportPath)) {

      DBG ('The AD export path does not exist, create')
      [void] (mkdir $adExportPath)
    }

    DBG ('Goin to export BitLocker recovery packages from AD: {0}' -f $adExportPath)
    DBGSTART
    $adExport = BitColdKit-ExportAdRecoveryInfo -folderPath $adExportPath -forceExtractAllMachines
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $adExport) -lt 1 }

    [string] $adExportCSV = Join-Path $adExportPath 'adexport.csv'
    DBG ('Save AD export information: {0}' -f $adExportCSV)
    $adExport | Export-Csv -Path $adExportCSV -Encoding Unicode -Delimiter "`t" -Force -NoTypeInformation
  }


  DBG ('BitLocker recovery and BitColdKit verification finished')
}


function global:Verify-OidEncoding ()
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  [System.Collections.ArrayList] $deList = @()

  $rootDSE = Get-DE 'RootDSE' ([ref] $deList)
  $configDN = GDES $rootDSE configurationNamingContext
  $schemaDN = GDES $rootDSE schemaNamingContext

  $srcRes = Get-ADSearch $configDN 'subTree' '(objectClass=pkiCertificateTemplate)' @('msPKI-Certificate-Application-Policy','msPKI-Cert-Template-OID','pKIExtendedKeyUsage')
    
  if ($srcRes.found) {
    
    foreach ($oneRes in $srcRes.result) {
      
      GSRS $oneRes 'msPKI-Certificate-Application-Policy' | ? { Is-ValidString $_ } | % { Split-MultiValue $_ } | % { $_ } | % { DBGIF ('Invalid OID de/coding: {0}' -f $_) { (Decode-BerOID (Encode-BerOID $_)) -ne $_ } }
      GSRS $oneRes 'msPKI-Cert-Template-OID' | ? { Is-ValidString $_ } | % { Split-MultiValue $_ } | % { $_ } | % { DBGIF ('Invalid OID de/coding: {0}' -f $_) { (Decode-BerOID (Encode-BerOID $_)) -ne $_ } }
      GSRS $oneRes 'pKIExtendedKeyUsage' | ? { Is-ValidString $_ } | % { Split-MultiValue $_ } | % { $_ } | % { DBGIF ('Invalid OID de/coding: {0}' -f $_) { (Decode-BerOID (Encode-BerOID $_)) -ne $_ } }
    }

    Dispose-ADSearch ([ref] $srcRes)
  }
  

  $srcRes = Get-ADSearch $schemaDN 'subTree' '(objectClass=attributeSchema)' @('attributeID','attributeSyntax')
    
  if ($srcRes.found) {
    
    foreach ($oneRes in $srcRes.result) {
      
      GSRS $oneRes 'attributeID' | ? { Is-ValidString $_ } | % { Split-MultiValue $_ } | % { $_ } | % { DBGIF ('Invalid OID de/coding: {0}' -f $_) { (Decode-BerOID (Encode-BerOID $_)) -ne $_ } }
      GSRS $oneRes 'attributeSyntax' | ? { Is-ValidString $_ } | % { Split-MultiValue $_ } | % { $_ } | % { DBGIF ('Invalid OID de/coding: {0}' -f $_) { (Decode-BerOID (Encode-BerOID $_)) -ne $_ } }
    }

    Dispose-ADSearch ([ref] $srcRes)
  }
  

  $srcRes = Get-ADSearch $schemaDN 'subTree' '(objectClass=classSchema)' @('governsID')
    
  if ($srcRes.found) {
    
    foreach ($oneRes in $srcRes.result) {
      
      GSRS $oneRes 'governsID' | ? { Is-ValidString $_ } | % { Split-MultiValue $_ } | % { $_ } | % { DBGIF ('Invalid OID de/coding: {0}' -f $_) { (Decode-BerOID (Encode-BerOID $_)) -ne $_ } }
    }

    Dispose-ADSearch ([ref] $srcRes)
  }
  
  Dispose-List ([ref] $deList)
}


function global:Split-Dn (
  [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string] $dn
  )
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  [Collections.ArrayList] $elements = @()
  
  [System.Text.RegularExpressions.MatchCollection] $matches = [regex]::Matches($dn, $global:rxDnElement)

  if ($matches.Count -lt 1) {

    DBGIF ('No DN elements parsed out of the DN: {0}' -f $dn) { $matches.Count -lt 1 }

  } else {

    foreach ($oneMatch in $matches) {

      [string] $prefix = $oneMatch.Groups[1].Value.TrimStart(' ')
      [string] $value = $oneMatch.Groups[2].Value

      DBGIF ('Weird RDN element: {0} | {1}' -f $prefix, $value) { ([string]::IsNullOrEmpty($prefix)) -or ([string]::IsNullOrEmpty($value)) }

      $oneElement = New-Object PSObject
      Add-Member -Input $oneElement -MemberType NoteProperty -Name prefix -Value $prefix
      Add-Member -Input $oneElement -MemberType NoteProperty -Name value -Value $value
      Add-Member -Input $oneElement -MemberType NoteProperty -Name rdn -Value ('{0}={1}' -f $prefix, $value)

      [void] $elements.Add($oneElement)
    }

    DBG ('DN split into elements: {0} | #{1} | {2}' -f $dn, $elements.Count, (($elements | select -Expand rdn) -join ' ; '))
    DBGIF $MyInvocation.MyCommand.Name { ((($elements | select -Expand rdn) -join ',') -ne $dn) -and ((($elements | select -Expand rdn) -join ', ') -ne $dn) }
  }

  return $elements
}


function global:Find-ExistingCert ([string] $friendly, [string] $subject, [string[]] $sans, [string[]] $ekus)
{
  DBG ('Check if we have the certificate already present: friendly = {0} | subj = {1} | sans = #{2} | {3} | ekus = {4}' -f $friendly, $subject, $sans.Length, ($sans -join ','), ($ekus -join ','))
  $existingCerts = $null
  $existingCerts = dir Cert:\LocalMachine\My
  DBG ('Certificates found in the local machine store: #{0}' -f (Get-CountSafe $existingCerts))
        
  # Note: parsing SANs require ASN decoding of the .Extensions['2.5.29.17'].RawData
  DBGIF 'SAN names cannot be parsed easily on PS 2.0' { ($sans.Length -gt 0) -and ($PSVersionTable['PSVersion'].Major -le 2) }

  [bool] $alreadyExists = $false
  [string] $foundCert = $null

  if ((Get-CountSafe $existingCerts) -gt 0) {

    foreach ($oneExistingCert in $existingCerts) {

      DBG ('One existing certificate to be checked: {0} | {1} | {2}' -f $oneExistingCert.Thumbprint, $oneExistingCert.Subject, $oneExistingCert.FriendlyName)

      [bool] $matchFriendly = ((Is-EmptyString $friendly) -or ($oneExistingCert.FriendlyName -eq $friendly))
           
      [object[]] $requestedSubjectElements = Split-Dn $subject
      [object[]] $existingSubjectElements = Split-Dn $oneExistingCert.Subject

      [bool] $allSubjectElementsMatch = $requestedSubjectElements.Length -eq $existingSubjectElements.Length

      if ($allSubjectElementsMatch) {
 
        for ($i = 0; $i -lt $requestedSubjectElements.Length; $i ++) {

          DBG ('Checking one RDN element: {0} | {1} | {2}' -f $oneExistingCert.Thumbprint, $requestedSubjectElements[$i].rdn, $existingSubjectElements[$i].rdn)
          $allSubjectElementsMatch = $allSubjectElementsMatch -and ($requestedSubjectElements[$i].rdn -eq $existingSubjectElements[$i].rdn)
        }
      }

      [string[]] $dnsNamesExisting = $oneExistingCert.DnsNameList | sort
      [string[]] $dnsNamesRequested = $sans | sort

      [bool] $allSANsMatch = $sans.Length -eq $oneExistingCert.DnsNameList.Count

      if ($allSANsMatch) {

        foreach ($oneSAN in $sans) {

          $allSANsMatch = $allSANsMatch -and (Contains-Safe $oneExistingCert.DnsNameList $oneSAN)
        }
      }

      [bool] $allEKUsExist = $true
      [string[]] $existingEKUs = $oneExistingCert.Extensions['2.5.29.37'].EnhancedKeyUsages | Select -Expand Value

      DBG ('EKUs found in certificate: #{0} | {1}' -f $existingEKUs.Length, ($existingEKUs -join ','))

      foreach ($oneEKU in $ekus) {

        $allEKUsExist = $allEKUsExist -and (Contains-Safe $existingEKUs $oneEKU)
      }

      DBG ('One cert matching: {0} | friendly = {1} | subject = {2} | sans = {3} | ekus = {4}' -f $oneExistingCert.Thumbprint, $matchFriendly, $allSubjectElementsMatch, $allSANsMatch, $allEKUsExist)
      $alreadyExists = $matchFriendly -and $allSubjectElementsMatch -and $allSANsMatch -and $allEKUsExist

      if ($alreadyExists) {

        $foundCert = $oneExistingCert.Thumbprint

        DBG ('We have hit an existing certificate: {0}' -f $foundCert)
        break
      }
    }
  }

  return $foundCert
}

function global:Enroll-AppCertificate ([System.Xml.XmlElement] $xml, [bool] $reuseExisting)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $xml }

  if (Is-NonNull $xml) {

    DBG ('Determine the parent template XML node')
    [System.Xml.XmlElement] $parentTemplate = $xml.psbase.ParentNode
    DBGIF $MyInvocation.MyCommand.Name { Is-Null $parentTemplate }
    DBG ('Parent template: {0} | {1}' -f $parentTemplate.template, $parentTemplate.instance)

    DBG ('Determine the CA XML node')
    [System.Xml.XmlElement] $caNode = (Get-FirstAppHostInInstance 'ca' $parentTemplate.instance 'vmConfig').ca
    DBGIF $MyInvocation.MyCommand.Name { Is-Null $caNode }

    DBG ('Determine the template name and the CA name: prefix = {0}' -f $caNode.templateNamePrefix)
    [string] $template = $parentTemplate.template.Replace('$namePrefix$', $caNode.templateNamePrefix)
    [string] $caFQDN = Get-FirstAppHostInInstance 'ca' $parentTemplate.instance 'fqdn'
    [string] $ca = '{0}\{1}' -f $caFQDN, $parentTemplate.instance

    DBG ('Template from a CA: {0} | {1} | {2}' -f $template, $caFQDN, $ca)
   

    $caWaitParams = Get-MachineWaitParams $caNode
    $ourWaitParams = Get-MachineWaitParams $xml
    
    DBG ('Wait until the CA machine is really ready: ca = {0} | ours = {1}' -f $caWaitParams, $ourWaitParams)
    if ($caWaitParams -ne $ourWaitParams) {

      Wait-Machine $caWaitParams
    }


    DBG ('We must wait until the Delayed Autostart AD CS service gets finnaly up: {0}' -f $caFQDN)
    Wait-Periodically -maxTrialCount 37 -sleepSec 17 -sleepMsg 'Waiting for ADCS delayed autostart to get up' -scriptBlockWhichReturnsTrueToStop { 
    
      if ($global:thisOSVersion -ge 6.3) {

        $res = Run-Process certutil ('-v -sid 22 -ping "{0}"' -f $caFQDN) -returnExitCode $true

      } else {

        $res = Run-Process certutil ('-v -ping "{0}"' -f $caFQDN) -returnExitCode $true
      }

      return ($res -eq 0)
    }


    DBG ('Wait finished but the CA might not be trusted yet')
    Run-Process certutil ('-pulse')


    if (($xml.type -eq 'srv') -or ($xml.type -eq 'ra')) {

      [string] $certObtained = $null

      [string] $friendly = $xml.friendly
      [string] $subject = $xml.subject
      [string[]] $sans = Split-MultiValue $xml.san

      DBGIF $MyInvocation.MyCommand.Name { ($xml.type -eq 'ra') -and ($sans.Length -gt 0) }

      if ($xml.type -eq 'srv') {

        [string[]] $ekus = @('1.3.6.1.5.5.7.3.1') # Note: Server Authentication

      } elseif ($xml.type -eq 'ra') {

        [string[]] $ekus = @('1.3.6.1.4.1.311.20.2.1') # Note: Certificate Request Agent aka Enrollment Agent
      }

      if ($subject -notmatch $global:rxDnElement) {

        $subject = 'CN={0}' -f $subject
      }

      if ($reuseExisting) {

        $certObtained = Find-ExistingCert -friendly $friendly -subject $subject -sans $sans -ekus $ekus
      }

      if (Is-EmptyString $certObtained) {

        $certObtained = Enroll-ServerAuthenticationCertificate -ca $ca -subjectDN $subject -sans $sans -friendlyName $friendly -template $template -machineKey $true -signatureOnly ((Parse-BoolSafe $xml.pfs) -or (Parse-BoolSafe $xml.signatureOnly)) -encryptionOnly (Parse-BoolSafe $xml.encryptionOnly) -exportable (Parse-BoolSafe $xml.exportable) -cryptoProvider $xml.provider -ekusToAssert $ekus
      }

      return $certObtained
    }
  }
}



function global:Is-DnsZoneUpdatable ([object] $zone)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $zone }

  [bool] $couldWeUpdate = $false

  if (Is-NonNull $zone) {

    [bool] $areWeRODC = Is-LocalComputerDomainController -rodcOnly $true
    $couldWeUpdate = ($zone.ZoneType -eq 1) -and ((-not $areWeRODC) -or (-not $zone.DsIntegrated))
    DBG ('The zone parameters in respect to RODC: areWeRODC = {0} | isZoneADIntegrated = {1}' -f $areWeRODC, $zone.DsIntegrated)
  }

  DBG ('The zone is updatable: {0}' -f $couldWeUpdate)
  return $couldWeUpdate
}


function global:Assert-CustomAccountOU ([string] $root, [string] $domainDN, [System.Xml.XmlElement] $credNode, [string] $ouSubDN, [ref] $svcGroup)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { (Is-Null $credNode) -and (Is-EmptyString $ouSubDN) }

  [string] $outOU = $root
  [System.Collections.ArrayList] $deList = @()


  [string] $subDN = [string]::Empty
  [string] $svcGroupRequested = [string]::Empty

  if (Is-NonNull $credNode) {

    DBGIF $MyInvocation.MyCommand.Name { Is-Null $credNode.psbase.ParentNode }

    #$subDN = $credNode.psbase.ParentNode.accountOU.subDN
    $accountOUNode = $credNode.SelectSingleNode('./ancestor::*/accountOU[@subDN]')
    
    $subDN = $accountOUNode.subDN
    $svcGroupRequested = $accountOUNode.svcGroup
  
  } else {

    $subDN = $ouSubDN
  }


  if (Is-ValidString $subDN) {

    [string] $fullOUDN = [string]::Empty
    
    if (Is-ValidString $root) {
    
      $fullOUDN = '{0},{1},{2}' -f $subDN, $root, $domainDN
      $outOU = '{0},{1}' -f $subDN, $root

    } else {

      $fullOUDN = '{0},{1}' -f $subDN, $domainDN
      $outOU = $subDN
    }

    DBG ('Custom account OU specified: {0} | {1}' -f $subDN, $fullOUDN)

    $domainDE = Get-DE $domainDN ([ref] $deList)
    
    DBG ('Checking if the OU already exists')
    $existingOU = Find-DE $domainDE 'distinguishedName' $fullOUDN '(objectClass=organizationalUnit)' ([ref] $deList)

    if (Is-Null $existingOU) {

      [string[]] $dnComponents = Split-DnComponents -fullDN $fullOUDN -domainDN $domainDN
      DBG ('Target OU does not exist, create the full path: {0}' -f $fullOUDN)
       
      if ((Get-CountSafe $dnComponents) -gt 0) {

        [System.Collections.ArrayList] $finalDnComponents = New-Object System.Collections.ArrayList
        Add-DNComponent -dnList $finalDnComponents -dn $dnComponents[($dnComponents.Length - 1)] -dnType 'organizationalUnit' -attrAndValues $null -ACEs $null -rootDN $domainDN

        for ($i = $dnComponents.Length - 2; $i -ge 0; $i--) {

          Add-DNComponent -dnList $finalDnComponents -dn $dnComponents[$i] -dnType 'organizationalUnit' -attrAndValues $null -ACEs $null -rootDN $null
        }

        [void] (Create-DNPath $finalDnComponents $domainDE ([ref] $deList))
      }
    }
  }


  if (Is-ValidString $svcGroupRequested) {

    DBG ('We are asked to also create a service account group for the custom OU: {0}' -f $svcGroupRequested)
    Create-Group -cn $svcGroupRequested -ou $outOU -domainDN $domainDN | Out-Null

    if (Is-NonNull $svcGroup) {

      DBG ('Also return the service group to the caller')
      $svcGroup.Value = $svcGroupRequested
    }
  }


  Dispose-List ([ref] $deList)
  return $outOU
}


function global:Query-BuildingMachine ([string] $hvGuest, [string] $regBuilder)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  [string[]] $ips = (Get-WMIQueryArray '.' $global:wmiFltValidNIC) | ? { Is-NonNull $_.IPAddress } | select -Expand IPAddress | ? { ($_ -like '?*.?*.?*.?*') -and ($_ -ne '0.0.0.0') }

  $networkMap = Get-NetworkMap
  
  Write-Host ('')
  Write-Host ('##########################################################################') -ForegroundColor $global:assertColor
  Write-Host ('##') -ForegroundColor $global:assertColor
  Write-Host ('## Select the machine that you want to build') -ForegroundColor $global:assertColor
  Write-Host ('##') -ForegroundColor $global:assertColor

  if (Is-ValidString $hvGuest) {

    Write-Host ('## Hyper-V VM name: {0}' -f $hvGuest) -ForegroundColor $global:assertColor
  }

  if (Is-ValidString $regBuilder) {

    Write-Host ('## Name from base image: {0}' -f $regBuilder) -ForegroundColor $global:assertColor
  }

  Write-Host ('## Local computer name: {0}' -f $global:thisComputerHost) -ForegroundColor $global:assertColor

  if (Is-ValidString $global:thisComputerDomain) {

    Write-Host ('## Computer is member of domain: {0}' -f $global:thisComputerDomain) -ForegroundColor $global:assertColor
  }

  if ($ips.Length -gt 0) {

    Write-Host ('## Current IP addresses: {0}' -f ($ips -join ', ')) -ForegroundColor $global:assertColor
  }
  
  Write-Host ('##') -ForegroundColor $global:assertColor

  Write-Host ('## Current autologon user: {0} \ {1}' -f ([Environment]::UserDomainName), ([Environment]::UserName)) -ForegroundColor $global:assertColor
  Write-Host ('##') -ForegroundColor $global:assertColor

  #
  #

  function DoArraysIntersect ([string[]] $firstArray, [string[]] $secondArray)
  {
    [bool] $result = $false

    foreach ($oneFromFirst in $firstArray) { 
    
      foreach ($oneFromSecond in $secondArray) {
    
        if ($oneFromSecond -eq $oneFromFirst) { 
     
          $result = $true
        }
      }
    } 

    return $result
  }



  #
  #

  [Collections.ArrayList] $namesOffered = @()
  [int] $i = 1
  [string] $defaultOffer = [string]::Empty
  
  $networkMap | Group-Object machine | % { 

    $oneName = $_.Name
    [string[]] $oneIPs = $_.Group | select -Expand ipAddress | ? { $_ -ne '-' }

    if (Is-EmptyString $defaultOffer) {

      if ($oneName -eq $global:thisComputerHost) {

        $defaultOffer = $i

      } elseif ($oneName -eq $regBuilder) {

        $defaultOffer = $i
    
      } elseif ($oneName -eq $hvGuest) {

        $defaultOffer = $i
      
      } else {
      
        if (DoArraysIntersect $ips $oneIPs) {

          $defaultOffer = $i
        }
      }
    }

    #[void] $namesOffered.Add(('{0};{1}' -f $oneName, ($oneIPs -join ', ')))
    Add-AskerChoice -choices $namesOffered -name $oneName -id $i -description ($oneIPs -join ', ')
    $i ++
  }

  $vmSelected = Ask-UserForValueWithChoices -query 'Select one machine from the following options' -choices $namesOffered -defaultChoiceId $defaultOffer

  #$autologonPwd = Ask-UserForValue -query ('Password for autologon ({0}\{1})' -f ([Environment]::UserDomainName), ([Environment]::UserName)) -defaultVal 'Pa$$w0rd'
  #
  #$global:phaseCfg.sevecekBuildup.login.autoLogin = [string] ([Environment]::UserName)
  #$global:phaseCfg.sevecekBuildup.login.pwd = [string] $autologonPwd
  #$global:phaseCfg.sevecekBuildup.login.domain = [string] ([Environment]::UserDomainName)
  #
  #DBG ('Autologin identity updated to: {0}' -f ($global:phaseCfg.sevecekBuildup.login | Out-String))

  return $vmSelected
}


function global:Wait-IfRequested ([string] $waitId)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  [bool] $waitedActually = $false
  [string] $waitFile = Join-Path $env:SystemDrive ('wait*{0}.txt' -f $waitId)

  if (Is-NonNull $vmConfig) {

    $waitInstruction = Is-NonNull ($vmConfig.SelectSingleNode(('./phase[@wait=''{0}'' and translate(@reallyWait,"TRUE","true")=''true'']' -f $waitId)))
  }

  if ((Test-Path $waitFile) -or $waitInstruction) {

    DBG ('Wait file/instruction found. Will not restart after current phase: {0}' -f $waitId)
    DBG ('Press ENTER to continue.')
    $waitedActually = $true
    Read-Host | Out-Null
  }

  return $waitedActually
}


function global:PossiblyBind-HttpsCertificatesUnderInstallAccount ([string] $app, [string] $vmName, [string] $iLogin, [string] $iDomain, [string] $iPwd)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $vmName }

  DBG ('Should do any HTTP.SYS configuration: {0}' -f (Is-ValidString $vmConfig.httpsBinding.instance))

  if ((Is-ValidString $vmConfig.httpsBinding.instance) -and (Do-SubPhase httpsys)) {

    [bool] $canUseInstallAccount = (Is-ValidString $iLogin) -and ((Is-LocalComputerDomainController) -or (-not (Is-LocalDomain $iDomain)))
    [bool] $canUseCurrentAccount = (Is-CurrentUser -domainAccess $true)
    DBG ('Account to enroll the certificates: canUseInstall = {0} | canUseCurrent = {1}' -f $canUseInstallAccount, $canUseCurrentAccount)
    DBGIF ('Enrollment for AD CS certificates needs a domain account: app = {0} | {1}\{2}' -f $app, $iDomain, $iLogin) { (-not $canUseInstallAccount) -and (-not $canUseCurrentAccount) }

    if ($canUseInstallAccount) {

      # Note: this one must run under domain account wich is at the same time member of local Administrators group
      #       in order to obtain the certificate template list from AD
      $buildLibExitCode = Run-Process 'powershell' ('-ExecutionPolicy Bypass -File "{0}" "{1}"' -f "$global:rootDir\buildup-httpsys.ps1", $vmName) $true $iLogin $iDomain $iPwd $false $null $true -showWindow
      Finish-SubPhase httpsys
    
    } elseif ($canUseCurrentAccount) {

      # Note: this one must run under domain account wich is at the same time member of local Administrators group
      #       in order to obtain the certificate template list from AD
      $buildLibExitCode = Run-Process 'powershell' ('-ExecutionPolicy Bypass -File "{0}" "{1}"' -f "$global:rootDir\buildup-httpsys.ps1", $vmName) $true $null $null $null $false $null $true -showWindow
      Finish-SubPhase httpsys
    }
  }
}


function global:Bind-HttpsCertificates ()
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  Run-Process netsh 'http show sslcert'

  $foundRequesters = $vmConfig.SelectNodes('./*//bindHttps')
  [hashtable] $requestors = @{}
  DBG ('Do we have any https binding requestors: {0}' -f (Get-CountSafe $foundRequesters))
  if ((Get-CountSafe $foundRequesters) -gt 0) {

    foreach ($oneFoundRequester in $foundRequesters) {

      DBG ('One https binding requestor: {0}' -f $oneFoundRequester.appTag)
      if (-not (Contains-Safe $requestors.Keys $oneFoundRequester.appTag)) {

        [void] $requestors.Add($oneFoundRequester.appTag, $false)
      }
    }
  }

  DBG ('Do we have any https binding requirements: {0}' -f (Is-NonNull $vmConfig.httpsBinding))
  if (Is-NonNull $vmConfig.httpsBinding) {

    DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $requestors.Keys) -lt 1 }
    if ((Get-CountSafe $requestors.Keys) -gt 0) {

      [System.Xml.XmlElement] $httpsBindings = $vmConfig.httpsBinding
      $issueCerts = $httpsBindings.SelectNodes('./cert/one[@appTag]')

      DBG ('Certificates to be issued and bound: {0}' -f (Get-CountSafe $issueCerts))
      DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $issueCerts) -lt 1 }

      if ((Get-CountSafe $issueCerts) -gt 0) {

        foreach ($oneIssueCert in $issueCerts) {

          DBG ('One TLS certificate to be issued: {0} | {1} | {2} | {3}' -f $oneIssueCert.appTag, $oneIssueCert.binding, $oneIssueCert.subject, $oneIssueCert.psbase.ParentNode.instance)
          DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $oneIssueCert.appTag }
          DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $oneIssueCert.binding }
        
          DBG ('Verify if anybody requests the binding really')
          DBGIF $MyInvocation.MyCommand.Name { Is-Null $requestors[$oneIssueCert.appTag] }

          if (Is-NonNull $requestors[$oneIssueCert.appTag]) {

            $requestors[$oneIssueCert.appTag] = $true

            #[string] $caInstance = $oneIssueCert.psbase.ParentNode.instance
            #DBG ('Wait for the TLS certificate CA to finish: {0}' -f $caInstance)
            #DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $caInstance }
            #Wait-Machine (Get-FirstAppHostInInstance 'ca' $caInstance 'waitParams')
 
            [string] $tlsCert = Enroll-AppCertificate $oneIssueCert
            DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $tlsCert }

            [string[]] $bindings = Split-MultiValue $oneIssueCert.binding
            DBG ('Going to bind the certificate with the following bindings: {0} | {1}' -f $tlsCert, ($bindings -join ','))
            
            foreach ($oneBinding in $bindings) {

              [string] $bindingIpHost = $oneBinding.SubString(0, $oneBinding.IndexOf('@'))
              [string] $bindingPort = $oneBinding.SubString(($oneBinding.IndexOf('@') + 1))
              [string] $appId = '{4dc3e181-e14b-4a21-b022-59fc669b0914}'

              DBG ('Doing a single binding: {0} | {1} | {2} | {3} | {4}' -f $tlsCert, $oneBinding, $bindingIpHost, $bindingPort, $appId)
              DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $bindingIpHost }
              DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $bindingPort }

              if (Is-IPv4OrIPv6Address $bindingIpHost) {

                Run-Process netsh ('http add sslcert ipport={0}:{1} certhash={2} appId={3} certstore=my' -f $bindingIpHost, $bindingPort, $tlsCert, $appId)

              } else {

                Run-Process netsh ('http add sslcert hostnameport={0}:{1} certhash={2} appId={3} certstorename=my' -f $bindingIpHost, $bindingPort, $tlsCert, $appId)
              }

              DBG ('Just assert the result and our code genrally')
              $httpsBindingsToAssert = Obtain-HttpSslBindings
              $httpsBindingKeyToAssert = '{0}:{1}' -f $bindingIpHost, $bindingPort
              DBGIF $MyInvocation.MyCommand.Name { -not (Contains-Safe $httpsBindingsToAssert.Keys $httpsBindingKeyToAssert) }
              DBGIF $MyInvocation.MyCommand.Name { $httpsBindingsToAssert[$httpsBindingKeyToAssert].cert -ne $tlsCert }
            }
          }
        }
      }

      foreach ($oneRequestor in $requestors.Keys) {

        DBGIF ('One requestor not satisfied: {0}' -f $oneRequestor) { -not $requestors[$oneRequestor] }
      }
    }
  }
}


function global:Reopen-SPWebApplication ([ref] $webAppRef)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { Is-Null $webAppRef }
  [Microsoft.SharePoint.Administration.SPWebApplication] $webApp = $webAppRef.Value
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $webApp }

  [string] $originalId = $webApp.Id
  [int] $originalVer = $webApp.Version
  # Note: sometimes we get update conflicts when doing the following updates so we restart with
  #       reobtaining the web application again. It is probably only the case of applications with extensions
  #       made previously, but no harm if we reopen always
  DBG ('Reobtain the web application object again to prevent chaotic update conflicts: {0} | {1} | origVer = {2}' -f $webApp.Url, $originalId, $originalVer)
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $webApp.Url }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $originalId }
  DBGSTART
  # Note: there is a weird error that says: "cannot convert object[] to SPWebApplication although there should be
  #       just one object returned from the following command. 
  [object[]] $webAppsArray = Get-SPWebApplication -Identity $webApp.Url
  DBGIF ('Weird result from the database: #{0} | {1}' -f $webAppsArray.Length, ($webAppsArray | Out-String)) { $webAppsArray.Length -ne 1 }
  $webApp = $null
  $webApp = $webAppsArray[0]
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
  DBG ('Web application reopened: {0} | {1} | {2} | newVer = {3}' -f $webApp.DisplayName, $webApp.Url, $webApp.Id, $webApp.Version)
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $webApp }
  DBGIF $MyInvocation.MyCommand.Name { $originalId -ne $webApp.Id }

  $webAppRef.Value = $webApp
}


function global:Add-ServerManagerServer ([string] $server)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { $global:thisOSVersionNumber -lt 6.2 }
  DBGIF $MyInvocation.MyCommand.Name { ('Workgroup Server', 'Member Server', 'BDC', 'PDC') -notcontains $global:thisOSRole }
  DBGIF $MyInvocation.MyCommand.Name { $server -notlike '?*.?*' }

  if (($global:thisOSVersionNumber -ge 6.2) -and (('Workgroup Server', 'Member Server', 'BDC', 'PDC') -contains $global:thisOSRole)) {

    $serverManagerExe = Join-Path $env:SystemRoot 'System32\ServerManager.exe'
    $serverListFilePath = Join-Path $env:USERPROFILE 'AppData\Roaming\Microsoft\Windows\ServerManager\ServerList.xml'
    DBG ('Start the ServerManager if not yet run: {0}' -f $serverListFilePath)
    DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path -Literal $serverManagerExe) }
    DBGIF $MyInvocation.MyCommand.Name { Is-NonNull (gwmi Win32_Process | ? { $_.ProcessName -eq 'ServerManager.exe' } | ? { $_.Path -ne $serverManagerExe }) }

    # Note: the server manager runs only in a single instance, so it would not start again
    #       if already running on the same desktop even under a different user account
    #       thus we always kill all of them first
    DBG ('Kill the ServerManager(s) if already running')
    DBGSTART
    Get-Process | ? { $_.Name -eq 'ServerManager' } | Stop-Process ?Force
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    if (-not (Test-Path -Literal $serverListFilePath)) {

      # Note: the ServerManager creates its config xml only when stopped
      DBG ('Start the ServerManger and wait for its main window to become idle')
      DBGSTART
      $serverManagerProc = $null
      $serverManagerProc = [System.Diagnostics.Process]::Start($serverManagerExe)
      Start-Sleep -Seconds 3
      [void] $serverManagerProc.WaitForInputIdle()
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND

      DBG ('Let the ServerManager close gracefully to let it create its config file')
      Wait-Periodically -maxTrialCount 39 -sleepSec 3 -sleepMsg 'Wait until ServerManager finally disappears' -scriptBlockWhichReturnsTrueToStop { 
      
        DBGSTART
        # Note: we must close the program gracefully in order to let it create its configuration file
        Get-Process | ? { $_.Name -eq 'ServerManager' } | % { $_.CloseMainWindow() } | Out-Null
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND

        return (Is-Null (Get-Process | ? { $_.Name -eq 'ServerManager' }))
      }
    }

    $serverListFilePathBackup = Get-DataFileApp ('serverList-backup-{0}' -f (Canonicalize-FileName $env:USERPROFILE)) -extension 'xml'
    DBG ('Backup the serverList.xml file: {0} | {1}' -f $serverListFilePath, $serverListFilePathBackup)
    DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path -Literal $serverListFilePath) }

    if (Test-Path -Literal $serverListFilePath) {

      DBGIF $MyInvocation.MyCommand.Name { Test-Path -Literal $serverListFilePathBackup }
      DBGSTART
      Copy-Item -Path $serverListFilePath -Destination $serverListFilePathBackup -Force
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND

      DBG ('Load the serverList.xml: {0}' -f $serverListFilePath)
      DBGSTART
      [XML] $serverListXml = $null
      $serverListXml = [XML] (cat $serverListFilePath)
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
      DBGIF $MyInvocation.MyCommand.Name { Is-Null $serverListXml }

      if (Is-NonNull $serverListXml) {

        DBGIF $MyInvocation.MyCommand.Name { Is-Null $serverListXml.ServerList }
        DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $serverListXml.ServerList.localhostName }
        DBGIF $MyInvocation.MyCommand.Name { Is-Null $serverListXml.ServerList.ServerInfo }
        DBGIF $MyInvocation.MyCommand.Name { Is-NonNull $serverListXml.SelectSingleNode(('//ServerInfo[translate(@name,"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz")="{0}"]' -f $server.ToLower())) }

        DBG ('Duplicate the first server info')
        DBGSTART
        $newServerInfo = $null
        $newServerInfo = $serverListXml.ServerList.SelectSingleNode('./*[name()="ServerInfo" and @name]').Clone()
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
        DBGIF $MyInvocation.MyCommand.Name { Is-Null $newServerInfo }

        DBG ('Update the new server info: {0}' -f $server)
        $newServerInfo.name = $server
        $newServerInfo.status = '2'
        $newServerInfo.lastUpdateTime = '0001-01-01T00:00:00'

        DBG ('Append the new server info to the previous list')
        DBGSTART
        [void] $serverListXml.ServerList.AppendChild($newServerInfo)
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND

        DBG ('Save the newly updated ServerList.xml back over the original')
        DBGSTART
        $serverListXml.Save($serverListFilePath)
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
      }
    }
  }
}


function global:Create-DebugLogShortcut ()
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  $dbgFolder = Split-Path -Parent (GETDBGFILENAME)
  DBG ('Debug folder: {0}' -f $dbgFolder)
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $dbgFolder }
  DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path -Literal $dbgFolder) }

  if ((Is-ValidString $dbgFolder) -and (Test-Path -Literal $dbgFolder)) {

    $logLink = Join-Path $global:libCommonParentDir 'log.lnk'
    DBG ('Log link shortcut: {0}' -f $logLink)
    DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $logLink }
    #DBGIF $MyInvocation.MyCommand.Name { Test-Path -Literal $logLink }

    #if ((Is-ValidString $logLink) -and (-not (Test-Path -Literal $logLink))) {
    if (Is-ValidString $logLink) {

      DBG ('Creating link to the debug folder: {0} | {1}' -f $dbgFolder, $logLink)
      DBGSTART
      $shellObject = New-Object -comObject WScript.Shell
      $shortcut = $shellObject.CreateShortcut($logLink)
      $shortcut.TargetPath = $dbgFolder
      $shortcut.Save()
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
    }
  }
}


function global:Define-MCaster ()
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  # Note: the code comes from the SevecekMcaster\McastSenderReceiver.cs as pure copy/paste
  #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

  $code = @'
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Text;
using System.Threading;

namespace Sevecek.VmBuilder
{
    public class McastSenderReceiver : IDisposable
    {
        public class MemberState
        {
            private string name;
            public string Name { get { lock (this) { return this.name; } } }
            public string ShortName { get { lock (this) { return MemberState.GetShortNameFromNameID(this.name); } } }

            private string family;
            public string Family { get { lock (this) { return this.family; } } }

            private string status;
            public string Status { get { lock (this) { return this.status; } } set { lock (this) { this.status = value; } } }

            private DateTime casted;
            public DateTime Casted { get { lock (this) { return this.casted; } } }

            private DateTime firstSeen;
            public DateTime FirstSeen { get { lock (this) { return this.firstSeen; } } }

            private UInt64 receivedCount;
            public UInt64 ReceivedCount { get { lock (this) { return this.receivedCount; } } }

            private int frequency;
            public int Frequency { get { lock (this) { return this.frequency; } } }

            private const char charSeparator = '|';
            private const char charEscape = '\\';
            public const string nameTokenSeparator = "@";

            public MemberState() : this(Environment.MachineName, String.Empty, String.Empty, DateTime.MinValue, DateTime.Now, 0, -1) { }
            public MemberState(string family, string status, int frequency) : this(Environment.MachineName, family, status, DateTime.MinValue, DateTime.Now, 0, frequency) { }
            public MemberState(string name, string family, string status, DateTime casted, DateTime firstSeen, UInt64 receivedCount, int frequency)
            {
                lock (this)
                {
                    this.name = name;
                    this.family = family;
                    this.status = status;
                    this.casted = casted;
                    this.firstSeen = firstSeen;
                    this.receivedCount = receivedCount;
                    this.frequency = frequency;
                }
            }

            public MemberState(string message, IPAddress ip, int port)
            {
                string[] tokens = new string[4];

                int i = 0;
                int j = 0;
                StringBuilder oneToken = new StringBuilder();
                while (i < tokens.Length)
                {
                    if ((j == message.Length) || (message[j] == charSeparator))
                    {
                        tokens[i] = oneToken.ToString();
                        i++;

                        oneToken = new StringBuilder();
                        j++;
                        
                        continue;
                    }

                    if (message[j] == charEscape)
                    {
                        j++;
                    }

                    oneToken.Append(message[j]);
                    j++;
                }

                string toBeName = tokens[1];

                if (ip != null)
                {
                    toBeName = GetNameIDorSuffix(toBeName, ip, port);
                }

                lock (this)
                {
                    this.name = toBeName;
                    this.family = tokens[0];
                    this.status = tokens[2];
                    this.receivedCount = 1;
                    this.casted = DateTime.Now;
                    this.firstSeen = DateTime.Now;
                    this.frequency = int.Parse(tokens[3]);
                }
            }

            private string Escape(string what)
            {
                StringBuilder escaped = new StringBuilder();

                for (int i = 0; i < what.Length; i++)
                {
                    if ((what[i] == charEscape) || (what[i] == charSeparator))
                    {
                        escaped.Append(charEscape);
                    }

                    escaped.Append(what[i]);
                }
                
                return escaped.ToString();
            }

            public static string GetNameIDorSuffix(string name, IPAddress ip, int port)
            {
                string nameId;

                if (String.IsNullOrEmpty(name))
                {
                    nameId = String.Format("{0}{1}{0}{2}", nameTokenSeparator, ip, port);
                }
                else
                {
                    nameId = String.Format("{1}{0}{2}{0}{3}", nameTokenSeparator, name, ip, port);
                }

                return nameId;
            }

            public static string GetIPfromNameID(string nameId)
            {
                int firstIdx = nameId.IndexOf(nameTokenSeparator);
                int lastIdx = nameId.LastIndexOf(nameTokenSeparator);
                
                return nameId.Substring(firstIdx + 1, lastIdx - firstIdx - 1);
            }

            public static string GetShortNameFromNameID(string nameId)
            {
                int firstIdx = nameId.IndexOf(nameTokenSeparator);

                if (firstIdx > 0) {

                  return nameId.Substring(0, firstIdx);

                } else {
                                
                  return null;
                }
            }

            public override string ToString()
            {
                string extFamily;
                string extName;
                string extStatus;
                string extFrequency;

                lock (this)
                {
                    extFamily = this.family;
                    extName = this.name;
                    extStatus = this.status;
                    extFrequency = this.frequency.ToString();
                }
                
                return String.Format("{1}{0}{2}{0}{3}{0}{4}", charSeparator, Escape(extFamily), Escape(extName), Escape(extStatus), Escape(extFrequency));
            }

            public virtual string ToString(int longestName, string separator)
            {
                string extFamily;
                string extName;
                string extStatus;
                int extFrequency;
                DateTime extFirstSeen;
                DateTime extCasted;
                ulong extReceiveCount;

                lock (this)
                {
                    extFamily = this.family;
                    extName = this.name;
                    extStatus = this.status;
                    extFrequency = this.frequency;
                    extFirstSeen = this.firstSeen;
                    extCasted = this.casted;
                    extReceiveCount = this.receivedCount;
                }

                return String.Format("family = {1} {0} freq = {2} {0} since = {3} {0} last = {4} ({5,6:D} sec) {0} # = {6:D6} {0} {7} {0} {8}", separator, extFamily, extFrequency, extFirstSeen.ToString("yyyy-dd-MM HH:mm:ss"), extCasted.ToString("yyyy-dd-MM HH:mm:ss"), (int)(DateTime.Now - extCasted).TotalSeconds, extReceiveCount, extName.PadRight(longestName, ' '), extStatus);
            }

            public MemberState Clone()
            {
                MemberState newClone;

                lock (this)
                {
                    newClone = new MemberState(this.name, this.family, this.status, this.casted, this.firstSeen, this.receivedCount, this.frequency);
                }

                return newClone;
            }

            public void Update()
            {
                lock (this)
                {
                    this.casted = DateTime.Now;
                    this.receivedCount++;
                }
            }

            public void Update(MemberState apply)
            {
                lock (this)
                {
                    if (this.family != apply.Family)
                    {
                        throw (new Exception(String.Format("Invalid family for update: {0} | {1} | {2}", this.name, this.family, apply.Family)));
                    }

                    if (this.name != apply.Name) 
                    {
                        throw (new Exception(String.Format("Invalid name for update: {0} | {1}", this.name, apply.Name)));
                    }

                    if (this.firstSeen > apply.FirstSeen)
                    {
                        throw (new Exception(String.Format("Invalid first seen datetime for update: {0} | {1:s} | {2:s}", this.name, this.firstSeen, apply.firstSeen)));
                    }

                    this.casted = DateTime.Now;
                    this.receivedCount += apply.ReceivedCount;
                    this.frequency = apply.Frequency;
                    this.status = apply.Status;
                }
            }
        }

        private class ReceptionInfo
        {
            public byte[] buffer;
            public int length;
            public EndPoint from = null;

            public ReceptionInfo(int bufferMax)
            {
                this.from = new IPEndPoint(IPAddress.Any, 0);
                this.buffer = new byte[bufferMax];
            }
        }

        public const int defaultFrequency = 10;
        public const int defaultPort = 33333;
        public const string defaultMCastIP = "239.255.33.33";
        public const int sendReceiveBufferMax = 250;
        public const int defaultMCastTTL = 1;

        private Object stateLock = new Object();
        private MemberState state = null;
        private IPAddress mcastIP = null;
        private IPEndPoint mcastEndpoint = null;
        private int mcastTTL = 0;

        private Object ipListLock = new Object();
        private List localIPs = new List();
        private List localIDs = new List();
        private Thread ipDetectionThread = null;

        private Object receivedStatesLock = new Object();
        private Dictionary receivedStates = new Dictionary();

        private Object senderLock = new Object();
        private Dictionary senderSockets = new Dictionary();
        private Dictionary senderThreads = new Dictionary();

        private Object receiverLock = new Object();
        private Dictionary receiverSockets = new Dictionary();
        private Dictionary receiverThreads = new Dictionary();

        private class ExceptionInfo
        {
            public Exception exception;
            public string note;
            public DateTime when;

            public ExceptionInfo(Exception exception, string note)
            {
                this.exception = exception;
                this.note = note;
                this.when = DateTime.Now;
            }
        }

        private List exceptions = new List();

        private enum OperationEndedReason
        {
            ok,
            ipDisappeared,
            anyOther
        }

        public McastSenderReceiver(string family, string status) : this(family, status, defaultFrequency, defaultPort, defaultMCastIP, defaultMCastTTL) { }
        public McastSenderReceiver(string family, string status, int frequency, int port, string mcastIp, int mcastTTL)
        {
            lock (this.stateLock)
            {
                this.state = new MemberState(family, status, frequency);
                this.mcastIP = IPAddress.Parse(mcastIp);
                this.mcastEndpoint = new IPEndPoint(this.mcastIP, port);
                this.mcastTTL = mcastTTL;
            }

            lock (this.ipListLock) {

                this.ipDetectionThread = new Thread(new System.Threading.ThreadStart(ThreadIpDetection));
                this.ipDetectionThread.Start();
            }
        }

        private string GetLocalID(Socket senderSocket)
        {
            string localName;

            lock (this.stateLock)
            {
                localName = this.state.Name;
            }

            return MemberState.GetNameIDorSuffix(localName, ((IPEndPoint)senderSocket.LocalEndPoint).Address, ((IPEndPoint)senderSocket.LocalEndPoint).Port);
        }

        public void ThreadIpDetection()
        {
            try
            {
                int frequency;
                IPAddress localMcastIP;
                int localMcastTTL;
                int localMcastPort;

                lock (this.stateLock)
                {
                    frequency = this.state.Frequency;
                    localMcastIP = this.mcastIP;
                    localMcastTTL = this.mcastTTL;
                    localMcastPort = this.mcastEndpoint.Port;
                }

                while (true)
                {
                    NetworkInterface[] nics = NetworkInterface.GetAllNetworkInterfaces();
                    List detectedLocalIPs = new List();

                    List missingLocalIPs;

                    lock (this.ipListLock)
                    {
                        missingLocalIPs = new List(this.localIPs);
                    }

                    foreach (NetworkInterface oneNic in nics)
                    {
                        if (oneNic.SupportsMulticast && !oneNic.IsReceiveOnly && (oneNic.OperationalStatus == OperationalStatus.Up))
                        {
                            UnicastIPAddressInformationCollection oneNicIPs = oneNic.GetIPProperties().UnicastAddresses;

                            foreach (UnicastIPAddressInformation oneNicIP in oneNicIPs)
                            {
                                if (oneNicIP.DuplicateAddressDetectionState == DuplicateAddressDetectionState.Preferred)
                                {
                                    if (oneNicIP.Address.AddressFamily == AddressFamily.InterNetwork)
                                    {
                                        string oneNicIPString = oneNicIP.Address.ToString();

                                        if (oneNicIPString != "127.0.0.1")
                                        {
                                            bool newDetection = false;

                                            lock (this.ipListLock)
                                            {
                                                newDetection = !this.localIPs.Contains(oneNicIPString);
                                            }

                                            if (newDetection)
                                            {
                                                detectedLocalIPs.Add(oneNicIPString);
                                            }
                                            else
                                            {
                                                missingLocalIPs.Remove(oneNicIPString);
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }

                    List localIDsToVerify;
                    lock (this.ipListLock)
                    {
                        localIDsToVerify = new List(this.localIDs);
                    }

                    lock (this.receivedStatesLock)
                    {
                        foreach (string oneLocalID in localIDsToVerify)
                        {
                            if ((this.receivedStates.ContainsKey(oneLocalID)) && (this.receivedStates[oneLocalID].Casted < DateTime.Now.AddSeconds(-2 * frequency)))
                            {
                                missingLocalIPs.Add(MemberState.GetIPfromNameID(oneLocalID));
                            }
                        }
                    }

                    foreach (string oneMissingLocalIP in missingLocalIPs)
                    {
                        lock (this.ipListLock)
                        {
                            lock (this.senderLock)
                            {
                                if (this.senderSockets.ContainsKey(oneMissingLocalIP))
                                {
                                    string oneLocalIDtoRemove = GetLocalID(this.senderSockets[oneMissingLocalIP]);

                                    this.senderSockets[oneMissingLocalIP].Close();
                                    this.senderSockets.Remove(oneMissingLocalIP);

                                    this.localIDs.Remove(oneLocalIDtoRemove);

                                    this.senderThreads[oneMissingLocalIP].Abort();
                                    this.senderThreads.Remove(oneMissingLocalIP);
                                }
                            }

                            lock (this.receiverLock)
                            {
                                if (this.receiverSockets.ContainsKey(oneMissingLocalIP))
                                {
                                    this.receiverSockets[oneMissingLocalIP].Close();
                                    this.receiverSockets.Remove(oneMissingLocalIP);

                                    this.receiverThreads[oneMissingLocalIP].Abort();
                                    this.receiverThreads.Remove(oneMissingLocalIP);
                                }
                            }

                            this.localIPs.Remove(oneMissingLocalIP);
                        }
                    }

                    foreach (string oneDetectedLocalIP in detectedLocalIPs)
                    {
                        try
                        {
                            lock (this.ipListLock)
                            {
                                this.localIPs.Add(oneDetectedLocalIP);

                                lock (this.senderLock)
                                {
                                    Socket senderSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);

                                    // Note: on Windows 2003/XP the bind operation must come yet before the SetSocketOption
                                    //       though weird, otherwise it raises the "invalid argument" exception
                                    senderSocket.Bind(new IPEndPoint(IPAddress.Parse(oneDetectedLocalIP), 0));
                                    senderSocket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(localMcastIP, IPAddress.Parse(oneDetectedLocalIP)));
                                    senderSocket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, localMcastTTL);

                                    this.senderSockets.Add(oneDetectedLocalIP, senderSocket);
                                    this.localIDs.Add(GetLocalID(senderSocket));

                                    Thread newSenderThread = new Thread(new System.Threading.ParameterizedThreadStart(ThreadStateSender));
                                    this.senderThreads.Add(oneDetectedLocalIP, newSenderThread);
                                    newSenderThread.Start(oneDetectedLocalIP);
                                }
                            }
                        }
                        catch (Exception exp)
                        {
                            lock (this.exceptions)
                            {
                                this.exceptions.Add(new ExceptionInfo(exp, String.Format("creating sender socket: {0}", oneDetectedLocalIP)));
                            }
                        }

                        try
                        {
                            lock (this.ipListLock)
                            {
                                lock (this.receiverLock)
                                {
                                    Socket receiverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
                                    receiverSocket.Bind(new IPEndPoint(IPAddress.Parse(oneDetectedLocalIP), localMcastPort));
                                    receiverSocket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(localMcastIP, IPAddress.Parse(oneDetectedLocalIP)));
                                    receiverSocket.ReceiveTimeout = 5678;

                                    this.receiverSockets.Add(oneDetectedLocalIP, receiverSocket);

                                    Thread newReceiverThread = new Thread(new System.Threading.ParameterizedThreadStart(ThreadStateReceiver));
                                    this.receiverThreads.Add(oneDetectedLocalIP, newReceiverThread);
                                    newReceiverThread.Start(oneDetectedLocalIP);
                                }
                            }
                        }
                        catch (Exception exp)
                        {
                            lock (this.exceptions)
                            {
                                this.exceptions.Add(new ExceptionInfo(exp, String.Format("creating receiver socket: {0}", oneDetectedLocalIP)));
                            }
                        }
                    }

                    Thread.Sleep(frequency * 1000);
                }
            }

            catch (Exception exp)
            {
                lock (this.exceptions)
                {
                    this.exceptions.Add(new ExceptionInfo(exp, String.Format("running IP detection thread")));
                }
            }
        }

        public void ThreadStateReceiver(object objIP)
        {
            string ip = (string)objIP;

            while (true)
            {
                OperationEndedReason operationStatus = ReceiveState(ip);

                if (operationStatus != OperationEndedReason.ok)
                {
                    /*lock (this.receiverLock)
                    {
                        this.receiverSockets[ip].Close();
                        this.receiverSockets.Remove(ip);

                        this.receiverThreads.Remove(ip);
                    }

                    if (operationStatus == OperationEndedReason.ipDisappeared)
                    {
                        lock (this.ipListLock)
                        {
                            this.localIPs.Remove(ip);
                        }
                    }*/

                    return;
                }
            }
        }

        public void ThreadStateSender(object objIP)
        {
            string ip = (string)objIP;

            int frequency;

            lock (this.stateLock)
            {
                frequency = this.state.Frequency;
            }

            Random rnd = new Random();
            int factor = rnd.Next(800, 1000);

            while (true)
            {
                OperationEndedReason operationStatus = SendState(ip);

                if (operationStatus != OperationEndedReason.ok)
                {
                    /*lock (this.senderLock)
                    {
                        this.senderSockets[ip].Close();
                        this.senderSockets.Remove(ip);

                        this.senderThreads.Remove(ip);
                    }

                    if (operationStatus == OperationEndedReason.ipDisappeared)
                    {
                        lock (this.ipListLock)
                        {
                            this.localIPs.Remove(ip);
                        }
                    }*/

                    return;
                }

                Thread.Sleep(frequency * factor);
            }
        }

        private OperationEndedReason SendState(string ip)
        {
            OperationEndedReason status = OperationEndedReason.anyOther;

            try
            {
                string toSend;
                IPEndPoint localMcastEndpoint;

                lock (this.stateLock)
                {
                    toSend = this.state.ToString();
                    localMcastEndpoint = this.mcastEndpoint;
                }

                byte[] toSendBytes = Encoding.ASCII.GetBytes(toSend);

                if (toSendBytes.Length > sendReceiveBufferMax)
                {
                    throw (new Exception(String.Format("Buffer overflow to be sent: {0}", toSendBytes.Length)));
                }

                lock (this.senderLock)
                {
                    this.senderSockets[ip].SendTo(toSendBytes, localMcastEndpoint);
                }

                lock (this.stateLock)
                {
                    this.state.Update();
                }

                status = OperationEndedReason.ok;
            }

            catch (Exception exp)
            {
                if (((exp is SocketException) && ((SocketException)exp).SocketErrorCode == SocketError.AddressNotAvailable))
                {
                    status = OperationEndedReason.ipDisappeared;
                }
            }

            return status;
        }

        private OperationEndedReason ReceiveState(string ip)
        {
            OperationEndedReason status = OperationEndedReason.anyOther;

            try
            {
                ReceptionInfo oneInfo = new ReceptionInfo(sendReceiveBufferMax);
                bool retryLock = true;
                // Note: it is a matter of releasing the receiverLock sometimes and not blocking it indefinitelly in case nothing comes
                while (retryLock)
                {
                    try
                    {
                        Socket receiveSocket;

                        lock (this.receiverLock)
                        {
                            receiveSocket = this.receiverSockets[ip];
                        }

                        // Note: weirdly, but the receiver socket does not produce an exception even when
                        //       its underlaying interface goes, it still appears like bound and working but when
                        //       the adapter goes back online with the same IP address it does not restart and
                        //       stays bound but receives nothing. 
                        oneInfo.length = receiveSocket.ReceiveFrom(oneInfo.buffer, ref oneInfo.from);

                        retryLock = false;
                    }
                    catch (SocketException sckExp)
                    {
                        if ((sckExp.ErrorCode != 10060) || (sckExp.SocketErrorCode != SocketError.TimedOut))
                        {
                            throw sckExp;
                        }
                    }
                }

                MemberState receivedState = new MemberState(Encoding.ASCII.GetString(oneInfo.buffer, 0, oneInfo.length), ((IPEndPoint)oneInfo.from).Address, ((IPEndPoint)oneInfo.from).Port);

                bool validReception = false;
                lock (this.stateLock)
                {
                    validReception = receivedState.Family == this.state.Family;
                }

                if (validReception)
                {

                    lock (this.receivedStatesLock)
                    {
                        if (this.receivedStates.ContainsKey(receivedState.Name))
                        {
                            this.receivedStates[receivedState.Name].Update(receivedState);
                        }
                        else
                        {
                            this.receivedStates.Add(receivedState.Name, receivedState);
                        }
                    }
                }

                status = OperationEndedReason.ok;
            }

            catch (Exception exp)
            {
                if (((exp is SocketException) && ((SocketException)exp).SocketErrorCode == SocketError.AddressNotAvailable))
                {
                    status = OperationEndedReason.ipDisappeared;
                }
            }

            return status;
        }

        public void Dispose()
        {
            try
            {
                lock (this.stateLock)
                {
                    lock (this.ipListLock)
                    {
                        if ((this.localIPs == null) || (this.localIPs.Count < 1))
                        {
                            throw (new Exception("No local IPs were mounted at the disposal time"));
                        }

                        if (this.localIPs.Count != this.localIDs.Count)
                        {
                            throw (new Exception("Inconsistent number of local IPs vs. local IDs"));
                        }

                        if ((this.ipDetectionThread.ThreadState != ThreadState.Running) && (this.ipDetectionThread.ThreadState != ThreadState.WaitSleepJoin))
                        {
                            throw (new Exception(String.Format("The IP detection thread in an invalid state during disposal: {0}", this.ipDetectionThread.ThreadState)));
                        }

                        lock (this.receiverLock)
                        {
                            if ((this.receiverSockets == null) || (this.receiverSockets.Count < 1))
                            {
                                throw (new Exception("Cannot dispose uninitialized receiver objects"));
                            }

                            if (this.receiverSockets.Count != this.localIPs.Count)
                            {
                                throw (new Exception("Some local IP has not been properly mounted by receiver at the disposal time"));
                            }

                            if ((this.receiverThreads == null) || (this.receiverThreads.Count < 1))
                            {
                                throw (new Exception("No receiver thread to dispose"));
                            }

                            if (this.receiverThreads.Count != this.receiverSockets.Count)
                            {
                                throw (new Exception("Some receiver thread does not have a valid receiver socket at the disposal time"));
                            }

                            foreach (Thread oneReceiverThread in this.receiverThreads.Values)
                            {
                                if ((oneReceiverThread.ThreadState != ThreadState.Running) && (oneReceiverThread.ThreadState != ThreadState.WaitSleepJoin))
                                {
                                    throw (new Exception("Invalid state for a receiver thread to dispose"));
                                }
                            }
                        }

                        lock (this.senderLock)
                        {
                            if ((this.senderSockets == null) || (this.senderSockets.Count < 1))
                            {
                                throw (new Exception("Cannot dispose uninitialized sender object"));
                            }

                            if (this.senderSockets.Count != this.localIPs.Count)
                            {
                                throw (new Exception("Some local IP has not been properly mounted by sender at the disposal time"));
                            }

                            if ((this.senderThreads == null) || (this.senderThreads.Count < 1))
                            {
                                throw (new Exception("No sender thread to dispose"));
                            }

                            if (this.senderThreads.Count != this.senderSockets.Count)
                            {
                                throw (new Exception("Some sender thread does not have a valid sender socket at the disposal time"));
                            }

                            foreach (Thread oneSenderThread in this.senderThreads.Values)
                            {
                                if ((oneSenderThread.ThreadState != ThreadState.Running) && (oneSenderThread.ThreadState != ThreadState.WaitSleepJoin))
                                {
                                    throw (new Exception("Invalid state for a sender thread to dispose"));
                                }
                            }
                        }
                    }
                }
            }

            catch (Exception exp)
            {
                lock (this.exceptions)
                {
                    this.exceptions.Add(new ExceptionInfo(exp, String.Format("predisposing checks")));
                }
            }

            //
            //

            try
            {
                lock (this.stateLock)
                {
                    lock (this.ipListLock)
                    {
                        lock (this.receiverLock)
                        {
                            foreach (Socket oneReceiverSocket in this.receiverSockets.Values)
                            {
                                oneReceiverSocket.Close();
                            }

                            this.receiverSockets.Clear();

                            foreach (Thread oneReceiverThread in this.receiverThreads.Values)
                            {
                                oneReceiverThread.Abort();
                            }

                            this.receiverThreads.Clear();
                        }

                        lock (this.senderLock)
                        {
                            foreach (Socket oneSenderSocket in this.senderSockets.Values)
                            {
                                oneSenderSocket.Close();
                            }

                            this.senderSockets.Clear();

                            foreach (Thread oneSenderThread in this.senderThreads.Values)
                            {
                                oneSenderThread.Abort();
                            }

                            this.senderThreads.Clear();
                        }

                        this.localIPs.Clear();
                        this.localIDs.Clear();

                        if (this.ipDetectionThread != null)
                        {
                            this.ipDetectionThread.Abort();
                            this.ipDetectionThread = null;
                        }
                    }
                }
            }

            catch (Exception exp)
            {
                lock (this.exceptions)
                {
                    this.exceptions.Add(new ExceptionInfo(exp, String.Format("disposing the multicaster object")));
                }
            }

            //
            //

            lock (this.exceptions)
            {
                if (this.exceptions.Count > 0)
                {
                    List exceptionsToProcess = new List();

                    foreach (ExceptionInfo oneException in this.exceptions)
                    {
                        if (!(oneException.exception is ThreadAbortException))
                        {
                            exceptionsToProcess.Add(oneException);
                        }
                    }

                    StringBuilder expText = new StringBuilder();
                    int i = 0;

                    foreach (ExceptionInfo oneException in exceptionsToProcess)
                    {
                        expText.AppendLine(String.Format("Error: #{0}: {1}: {2}: {3}", i, oneException.when.ToString("yyyy-MM-dd HH:mm:ss"), oneException.note, oneException.exception.ToString()));
                        i++;
                    }

                    if (expText.Length > 0)
                    {
                        throw (new Exception(String.Format("Exceptions in worker threads: {0}\r\n{1}", exceptionsToProcess.Count, expText.ToString())));
                    }
                }
            }
        }

        public MemberState LocalState
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.state.Clone();
                }
            }
        }

        public string Status
        {
            set
            {
                lock (this.stateLock)
                {
                    this.state.Status = value;
                }
            }
        }

        public List CollectedStates
        {
            get
            {
                List listCopy = new List();

                lock (this.receivedStatesLock)
                {
                    List sortedMemberStates = new List(this.receivedStates.Keys);
                    sortedMemberStates.Sort();

                    foreach (string oneSortedMemberState in sortedMemberStates)
                    {
                        listCopy.Add(this.receivedStates[oneSortedMemberState]);
                    }
                }

                List deepCopy = new List();

                foreach (MemberState oneListCopyMember in listCopy)
                {
                    deepCopy.Add(oneListCopyMember.Clone());
                }

                return deepCopy;
            }
        }

        public virtual string ToString(string separator)
        {
            StringBuilder outStr = new StringBuilder();
            List collectedStates = this.CollectedStates;

            List localIPList;
            List localIDList;
            lock (this.ipListLock)
            {
                localIPList = new List(this.localIPs);
                localIDList = new List(this.localIDs);
            }

            StringBuilder strLocalIPs = new StringBuilder();
            StringBuilder strLocalIDs = new StringBuilder();

            foreach (string oneLocalIP in localIPList)
            {
                if (strLocalIPs.Length > 0)
                {
                    strLocalIPs.Append(", ");
                }

                strLocalIPs.Append(oneLocalIP);
            }

            foreach (string oneLocalID in localIDList)
            {
                if (strLocalIDs.Length > 0)
                {
                    strLocalIDs.Append(", ");
                }

                strLocalIDs.Append(oneLocalID);
            }

            int noSenderSockets;
            int noReceiverSockets;

            lock (this.senderLock)
            {
                noSenderSockets = this.senderSockets.Count;
            }

            lock (this.receiverLock)
            {
                noReceiverSockets = this.receiverSockets.Count;
            }

            // Note: no other locking just to be faster
            lock (this.stateLock)
            {
                outStr.AppendLine(String.Format("mcast = {1}:{2} {0} ttl = {3} {0} localIPs = {4}", separator, this.mcastEndpoint.Address, this.mcastEndpoint.Port, this.mcastTTL, strLocalIPs));
                outStr.AppendLine(String.Format("our family = {1} {0} name = {2} {0} collected states: # = {3}", separator, this.state.Family, this.state.Name, collectedStates.Count));
                outStr.AppendLine(String.Format("senderSck = {1} {0} receiverSck = {2} {0} sender IDs: {3}", separator, noSenderSockets, noReceiverSockets, strLocalIDs));
                outStr.AppendLine("");
            }

            ArrayList uniqueMemberNames = new ArrayList();

            foreach (MemberState oneCollectedState in collectedStates)
            {
               string oneMemberShortName = oneCollectedState.ShortName;

               if (!String.IsNullOrEmpty(oneMemberShortName)) {
                
                 if (!uniqueMemberNames.Contains(oneMemberShortName.ToUpper())) {

                   uniqueMemberNames.Add(oneMemberShortName);
                 }
               }
            }

            uniqueMemberNames.Sort();

            outStr.AppendLine(String.Format("members brief: {0}", String.Join(", ", uniqueMemberNames.ToArray())));
            outStr.AppendLine("");

            int longestName = 0;
            foreach (MemberState oneCollectedState in collectedStates)
            {
                longestName = Math.Max(longestName, oneCollectedState.Name.Length);
            }

            foreach (MemberState oneCollectedState in collectedStates)
            {
                outStr.AppendLine(oneCollectedState.ToString(longestName, separator));
            }

            return outStr.ToString();
        }

        public override string ToString()
        {
            return this.ToString("|");
        }
    }
}
'@

  #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  # Note: the code comes from the SevecekMcaster\McastSenderReceiver.cs as pure copy/paste
  #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

  if (-not ('Sevecek.VmBuilder.McastSenderReceiver' -as [Type])) {

    Add-Type -TypeDefinition $code
  }
}


function global:Obscure-AsciiText ([string] $asciiText)
{
  [byte[]] $bytes = [System.Text.ASCIIEncoding]::ASCII.GetBytes($asciiText)

  for ($i = 0; $i -lt $bytes.Length; $i ++) {

    $bytes[$i] += 37
  }

  return ([Convert]::ToBase64String($bytes))
}


function global:Unobscure-AsciiText ([string] $base64obscured)
{
  [byte[]] $bytes = [Convert]::FromBase64String($base64obscured)

  for ($i = 0; $i -lt $bytes.Length; $i ++) {

    $bytes[$i] -= 37
  }

  return ([System.Text.ASCIIEncoding]::ASCII.GetString($bytes))
}


function global:Send-OutlookEwsMail ([string] $lgn, [string] $psd, [switch] $credObscured, [string] $rcptTo, [string] $subject, [string] $message, [string] $dllPath, [switch] $resilient, [string[]] $attachments)
{
  #DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  if (Is-EmptyString $dllPath) {

    #DBG ('Defaulting the DLL path to libcommon parent: {0}')
    $dllPath = Join-Path $global:libCommonParentDir 'ForeignBinaries\EWS\2014-04-11'
  }

  if ($resilient) {

    $retryCount = 17

  } else {

    $retryCount = 1
  }

  $ewsAssemblyPath = Join-Path $dllPath 'Microsoft.Exchange.WebServices.dll'
  #DBG ('Loading the EWS assembly: {0}' -f $ewsAssemblyPath)
  DBGIF ('Cannot find EWS assembly file: {0}' -f $ewsAssemblyPath) { -not (Test-Path -Literal $ewsAssemblyPath) }

  if (Test-Path -Literal $ewsAssemblyPath) {

    DBGSTART
    $exchAssembly = $null
    $exchAssembly = [System.Reflection.Assembly]::LoadFile($ewsAssemblyPath)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBGIF $MyInvocation.MyCommand.Name { Is-Null $exchAssembly }

    if (Is-NonNull $exchAssembly) {
     
      if ($credObscured) {

        $realLgn = Unobscure-AsciiText $lgn
        $realPsd = Unobscure-AsciiText $psd

      } else {

        $realLgn = $lgn
        $realPsd = $psd
      }

      $outlookCom = 'outlook.com'
      DBGIF $MyInvocation.MyCommand.Name { -not (Test-Dns $outlookCom 443) }

      DBGIF ('EWS requires TLS 1.0') { (Test-Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Client') -and ((Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Client').Enabled -eq 0) }
      DBGIF ('EWS requires TLS 1.0') { (Test-Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Client') -and ((Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Client').DisabledByDefault -eq 1) }

      ##

    Wait-Periodically -maxTrialCount $retryCount -sleepSec 17 -randomizeSleepPercent 70 -sleepMsg 'Previous mailing attempt failed' -sleepMsgOnlyIfNotFinishedImmediatelly -scriptBlockWhichReturnsTrueToStop {

      [bool] $success = $false

      #DBG ('Create the EWS web service class')
      DBGSTART
      $exchSvc = $null
      $exchSvc = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService
      $exchSvc.Credentials = New-Object Microsoft.Exchange.WebServices.Data.WebCredentials($realLgn, $realPsd)
      $exchSvc.Url = New-Object System.Uri ('https://{0}/EWS/Exchange.asmx' -f $outlookCom)
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
      DBGIF $MyInvocation.MyCommand.Name { Is-Null $exchSvc }

      if (Is-NonNull $exchSvc) {

        $message = [regex]::Replace($message, '\r\n?|\n', '
'); #DBG ('Create the email message with the EWS service') DBGSTART $exchMail = $null $exchMail = New-Object Microsoft.Exchange.WebServices.Data.EmailMessage($exchSvc) [void] $exchMail.ToRecipients.Add($rcptTo) $exchMail.Subject = $subject $exchMail.Body = New-Object Microsoft.Exchange.WebServices.Data.MessageBody ($message) DBGER $MyInvocation.MyCommand.Name $error DBGEND DBGIF $MyInvocation.MyCommand.Name { Is-Null $exchMail } #DBG ('Should include attachments: #{0}' -f (Get-CountSafe $attachments)) if ((Get-CountSafe $attachments) -gt 0) { foreach ($oneAttachement in $attachments) { if (Is-ValidString $oneAttachement) { DBG ('One attachement to include: {0}' -f $oneAttachement) DBGIF ('One attachement does not exit: {0}' -f $oneAttachement) { -not (Test-Path -Literal $oneAttachement) } if (Test-Path -Literal $oneAttachement) { DBGSTART [void] $exchMail.Attachments.AddFileAttachment(([IO.Path]::GetFileName($oneAttachement)), $oneAttachement) DBGER $MyInvocation.MyCommand.Name $error DBGEND } } } } if (Is-NonNull $exchMail) { DBG ('Going to do the homework') DBGSTART # Note: it is probably an outside of the PowerShell exception that would raise in a too much powerfull way :-) try { $exchMail.Send() } catch { } if ($error.Count -ne 0) { # Note: unfinished wait asserts on its own DBGER $MyInvocation.MyCommand.Name $error -silent } else { $success = $true } DBGEND } } return $success } # wait } } } function global:Restore-IISSiteAndPoolState ([hashtable] $iisSitesBackup, [hashtable] $iisAppPoolBackup) { DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator)) DBGIF $MyInvocation.MyCommand.Name { Is-Null $iisSitesBackup } DBGIF $MyInvocation.MyCommand.Name { Is-Null $iisAppPoolBackup } DBG ('Try restoring anything that was stopped during the buildup accidentally') $iisSitesAfterBuildup = Get-IISSites -structured $true $iisAppPoolAfterBuildup = Get-IISAppPools -structured $true $appCmd = Join-Path $env:SystemRoot 'System32\inetsrv\appcmd.exe' DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path -Literal $appCmd) } DBG ('IIS sites: backup = {0} | now = {1}' -f $iisSitesBackup.Count, $iisSitesAfterBuildup.Count) DBG ('IIS pools: backup = {0} | now = {1}' -f $iisAppPoolBackup.Count, $iisAppPoolAfterBuildup.Count) DBG ('Verify the sites first') foreach ($oneIisSiteBackup in $iisSitesBackup.Keys) { $theIisSiteBackup = $iisSitesBackup[$oneIisSiteBackup] $theIisSiteAfterBuildup = $iisSitesAfterBuildup[$oneIisSiteBackup] DBGIF ('Site disappeared: {0}' -f $oneIisSiteBackup) { Is-Null $theIisSiteAfterBuildup } if (Is-NonNull $theIisSiteAfterBuildup) { DBGIF ('Site with different bindings: {0} | pre = {1} | post = {2}' -f $oneIisSiteBackup, $theIisSiteBackup.bindings, $theIisSiteAfterBuildup.bindings) { $theIisSiteBackup.bindings -ne $theIisSiteAfterBuildup.bindings } DBGIF ('Site started: {0}' -f $oneIisSiteBackup) { ($theIisSiteBackup.state -ne 'Started') -and ($theIisSiteAfterBuildup.state -ne $theIisSiteBackup.state) } if (($theIisSiteBackup.state -eq 'Started') -and ($theIisSiteAfterBuildup.state -ne 'Started')) { DBG ('One SITE STOPPED during buildup, starting: {0}' -f $oneIisSiteBackup) Run-Process $appCmd ('set site "{0}" /serverAutoStart:true /commit:appHost' -f $oneIisSiteBackup) Run-Process $appCmd ('start site /site.name:"{0}"' -f $oneIisSiteBackup) } } } DBG ('Proceed with app pools now') foreach ($oneIisAppPoolBackup in $iisAppPoolBackup.Keys) { $theIisAppPoolBackup = $iisAppPoolBackup[$oneIisAppPoolBackup] $theIisAppPoolAfterBuildup = $iisAppPoolAfterBuildup[$oneIisAppPoolBackup] DBGIF ('AppPool disappeared: {0}' -f $oneIisAppPoolBackup) { Is-Null $theIisAppPoolAfterBuildup } if (Is-NonNull $theIisAppPoolAfterBuildup) { DBGIF ('AppPool with different netfx: {0} | pre = {1} | post = {2}' -f $oneIisAppPoolBackup, $theIisAppPoolBackup.netfx, $theIisAppPoolAfterBuildup.netfx) { $theIisAppPoolBackup.netfx -ne $theIisAppPoolAfterBuildup.netfx } DBGIF ('AppPool with different pipe: {0} | pre = {1} | post = {2}' -f $oneIisAppPoolBackup, $theIisAppPoolBackup.pipe, $theIisAppPoolAfterBuildup.pipe) { $theIisAppPoolBackup.pipe -ne $theIisAppPoolAfterBuildup.pipe } DBGIF ('AppPool started: {0}' -f $oneIisAppPoolBackup) { ($theIisAppPoolBackup.state -ne 'Started') -and ($theIisAppPoolAfterBuildup.state -ne $theIisAppPoolBackup.state) } if (($theIisAppPoolBackup.state -eq 'Started') -and ($theIisAppPoolAfterBuildup.state -ne 'Started')) { DBG ('One APPPOOL STOPPED during buildup, starting: {0}' -f $oneIisAppPoolBackup) Run-Process $appCmd ('set apppool "{0}" /autostart:true /commit:appHost' -f $oneIisAppPoolBackup) Run-Process $appCmd ('start apppool /apppool.name:"{0}"' -f $oneIisAppPoolBackup) } } } } function global:Extract-PasswordsFromConfig ([string] $outputCSV, [string] $hostAdmin = [Environment]::UserName, [string] $hostAdminDomain = [Environment]::UserDomainName, [string[]] $hostMachineIds = @($global:thisComputerFQDN)) { DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator)) DBGIF $MyInvocation.MyCommand.Name { Is-Null $global:xmlConfig } if (Is-NonNull $global:xmlConfig) { # # function New-AccountInfo ([string] $kind, [string] $login, [string] $domain, [string] $password, [string] $where) { $account = New-Object PSObject Add-Member -Input $account -MemberType NoteProperty -Name kind -Value $kind Add-Member -Input $account -MemberType NoteProperty -Name login -Value $login Add-Member -Input $account -MemberType NoteProperty -Name domain -Value $domain Add-Member -Input $account -MemberType NoteProperty -Name pwd -Value $password Add-Member -Input $account -MemberType NoteProperty -Name where -Value $where return $account } # # $accountHosts = $global:xmlConfig.SelectNodes('./VMs/HOST[@defaultAdminPwd and string-length(@defaultAdminPwd)!=0]') #DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $accountHost) -lt 1 } $accountsBuiltin = $global:xmlConfig.SelectNodes('./VMs/MACHINE[vm/@do="true"]/commonVM[@builtinAdminPwd and string-length(@builtinAdminPwd)!=0]') $accountsLocals = $global:xmlConfig.SelectNodes('./VMs/MACHINE[vm/@do="true"]/commonVM[@builtinUserPwd and string-length(@builtinUserPwd)!=0]') #DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $accountsBuiltin) -lt 1 } #DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $accountsLocals) -lt 1 } $accountsAdmI = $global:xmlConfig.SelectNodes('./VMs/MACHINE[vm/@do="true"]//app[@iLogin and string-length(@iLogin)!=0]') $accountsAdmA = $global:xmlConfig.SelectNodes('./VMs/MACHINE[vm/@do="true"]//app[@aLogin and string-length(@aLogin)!=0]') $accountsSvc = $global:xmlConfig.SelectNodes('./VMs/MACHINE[vm/@do="true"]//svc[@login and string-length(@login)!=0]') $accountsArbitrary = $global:xmlConfig.SelectNodes('./VMs/MACHINE[vm/@do="true"]//account[@login and string-length(@login)!=0]') $accountsForitify = $global:xmlConfig.SelectNodes('./VMs/MACHINE[vm/@do="true"]/fortifiedHost[@limitedUser and string-length(@limitedUser)!=0]') #DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $accountsAdmI) -lt 1 } #DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $accountsAdmA) -lt 1 } #DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $accountsSvc) -lt 1 } #DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $accountsArbitrary) -lt 1 } $secretsGeneric = $global:xmlConfig.SelectNodes('./macro[@pwdHint="true"]') $accountsDcPromo = $global:xmlConfig.SelectNodes('./VMs/MACHINE[vm/@do="true"]/domain[@dcPromo and string-length(@dcPromo)!=0]') $accountsDA = $global:xmlConfig.SelectNodes('./VMs/MACHINE[vm/@do="true"]/domain[@new and string-length(@new)!=0 and @domainAdmin and string-length(@domainAdmin)!=0]') $accountsDcImport = $global:xmlConfig.SelectNodes('./VMs/MACHINE[vm/@do="true"]/domain[@new and string-length(@new)!=0 and @userPwd and string-length(@userPwd)!=0]') #$accountsDA500 = $global:xmlConfig.SelectNodes('./VMs/MACHINE[vm/@do="true"]/domain[@new and string-length(@new)!=0]/originalAdmin') #DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $accountsDcPromo) -lt 1 } #DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $accountsDA) -lt 1 } #DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $accountsDcImport) -lt 1 } $svcSharePointPhrases = $global:xmlConfig.SelectNodes('./VMs/MACHINE[vm/@do="true"]/sp[@pass and string-length(@pass)!=0]') $svcSharePointSecStore = $global:xmlConfig.SelectNodes('./VMs/MACHINE[vm/@do="true"]/sp//svc[@appTag="sec"]/binding[@masterPwd and string-length(@masterPwd)!=0]') #DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $svcSharePointPhrases) -lt 1 } $fortifiedHosts = $global:xmlConfig.SelectNodes('./VMs/HOST/fortifiedHost[@limitedUser and string-length(@limitedUser)!=0]') $accountsDtrAutologon = $global:xmlConfig.SelectNodes('./VMs/DETERIORATE/seq/autoLogon[@login and string-length(@login)!=0]') # # $localMachineMoniker = '' if ((Is-LocalDomain $hostAdminDomain $true) -or (Is-BuiltinDomain $hostAdminDomain)) { $hostAdminDomain = $localMachineMoniker } # # [Collections.ArrayList] $outAccounts = @() if ((Get-CountSafe $accountHosts) -gt 0) { foreach ($oneAccountHost in $accountHosts) { foreach ($oneHostMachineId in $hostMachineIds) { $targettingApplied = $oneAccountHost.target.hostName #$hostAdminDomainUse = $hostAdminDomain $oneHostMachineIdUse = $oneHostMachineId if (Is-ValidString $targettingApplied) { #$hostAdminDomainUse = '{0} ({1})' -f $hostAdminDomainUse, $targettingApplied $oneHostMachineIdUse = '{0} ({1})' -f $oneHostMachineIdUse, $targettingApplied } [void] $outAccounts.Add((New-AccountInfo -kind 'hostAdmin' -login $hostAdmin -domain $null -password $oneAccountHost.defaultAdminPwd -where $oneHostMachineIdUse)) } } } if ((Get-CountSafe $accountsBuiltin) -gt 0) { foreach ($oneAccountBuiltin in $accountsBuiltin) { [void] $outAccounts.Add((New-AccountInfo -kind 'builtinAdmin' -login $oneAccountBuiltin.builtinAdmin -domain $null -password $oneAccountBuiltin.builtinAdminPwd -where (Get-MachineFQDN $oneAccountBuiltin -doNotAssertFQDN))) } } if ((Get-CountSafe $accountsLocals) -gt 0) { foreach ($oneAccountLocals in $accountsLocals) { [void] $outAccounts.Add((New-AccountInfo -kind 'localUsers' -login $null -domain $null -password $oneAccountLocals.builtinUserPwd -where (Get-MachineFQDN $oneAccountLocals -doNotAssertFQDN))) } } if ((Get-CountSafe $accountsAdmI) -gt 0) { foreach ($oneAccountAdmI in $accountsAdmI) { [void] $outAccounts.Add((New-AccountInfo -kind 'installAdmin' -login $oneAccountAdmI.iLogin -domain $oneAccountAdmI.iDomain -password $oneAccountAdmI.iPwd -where (Get-MachineFQDN $oneAccountAdmI -doNotAssertFQDN))) } } if ((Get-CountSafe $accountsAdmA) -gt 0) { foreach ($oneAccountAdmA in $accountsAdmA) { [void] $outAccounts.Add((New-AccountInfo -kind 'adminAccount' -login $oneAccountAdmA.aLogin -domain $oneAccountAdmA.iDomain -password $oneAccountAdmA.aPwd -where (Get-MachineFQDN $oneAccountAdmA -doNotAssertFQDN))) } } if ((Get-CountSafe $accountsSvc) -gt 0) { foreach ($oneAccountSvc in $accountsSvc) { [void] $outAccounts.Add((New-AccountInfo -kind 'serviceAccount' -login $oneAccountSvc.login -domain $oneAccountSvc.domain -password $oneAccountSvc.pwd -where (Get-MachineFQDN $oneAccountSvc -doNotAssertFQDN))) } } if ((Get-CountSafe $accountsArbitrary) -gt 0) { foreach ($oneAccountArbitrary in $accountsArbitrary) { [void] $outAccounts.Add((New-AccountInfo -kind 'account' -login $oneAccountArbitrary.login -domain $oneAccountArbitrary.domain -password $oneAccountArbitrary.pwd -where (Get-MachineFQDN $oneAccountArbitrary -doNotAssertFQDN))) } } if ((Get-CountSafe $accountsForitify) -gt 0) { foreach ($oneAccountFortify in $accountsForitify) { [void] $outAccounts.Add((New-AccountInfo -kind 'vmFortification' -login $oneAccountFortify.limitedUser -domain $null -password $oneAccountFortify.limitedUserPwd -where (Get-MachineFQDN $oneAccountFortify -doNotAssertFQDN))) } } if ((Get-CountSafe $secretsGeneric) -gt 0) { foreach ($oneSecretGeneric in $secretsGeneric) { [void] $outAccounts.Add((New-AccountInfo -kind 'secret' -login $null -domain $null -password $oneSecretGeneric.value -where $oneSecretGeneric.ref)) } } if ((Get-CountSafe $accountsDcPromo) -gt 0) { foreach ($oneAccountDcPromo in $accountsDcPromo) { [string] $safeModeAdminPwd = [string]::Empty foreach ($oneDCPromoParameter in (Split-MultiValue $oneAccountDcPromo.dcPromo)) { if ($oneDCPromoParameter -like '/SafeModeAdminPassword:?*') { $safeModeAdminPwd = $oneDCPromoParameter.SubString(23) break } } DBGIF ('No SafeAdminPwd for a DCPromo: {0}' -f $oneAccountDcPromo.dcPromo) { Is-EmptyString $safeModeAdminPwd } [void] $outAccounts.Add((New-AccountInfo -kind 'dsrm' -login $null -domain $null -password $safeModeAdminPwd -where (Get-MachineFQDN $oneAccountDcPromo -doNotAssertFQDN))) if (Is-ValidString $oneAccountDcPromo.login) { [void] $outAccounts.Add((New-AccountInfo -kind 'subDcPromo' -login $oneAccountDcPromo.login -domain $oneAccountDcPromo.join -password $oneAccountDcPromo.pwd -where (Get-MachineFQDN $oneAccountDcPromo -doNotAssertFQDN))) } } } if ((Get-CountSafe $accountsDA) -gt 0) { foreach ($oneAccountDA in $accountsDA) { $domain = Get-MachineDomain $oneAccountDA [void] $outAccounts.Add((New-AccountInfo -kind 'domainAdmin' -login $oneAccountDA.domainAdmin -domain $domain -password $oneAccountDA.adminPwd -where $domain)) } } if ((Get-CountSafe $accountsDcImport) -gt 0) { foreach ($oneAccountDcImport in $accountsDcImport) { $domain = Get-MachineDomain $oneAccountDcImport [void] $outAccounts.Add((New-AccountInfo -kind 'domainUsers' -login $null -domain $domain -password $oneAccountDA.userPwd -where $domain)) } } if ((Get-CountSafe $svcSharePointPhrases) -gt 0) { foreach ($oneSvcSharePointPhrase in $svcSharePointPhrases) { [void] $outAccounts.Add((New-AccountInfo -kind 'spPassphrase' -login $null -domain $null -password $oneSvcSharePointPhrase.pass -where (Get-MachineFQDN $oneSvcSharePointPhrase -doNotAssertFQDN))) } } if ((Get-CountSafe $svcSharePointSecStore) -gt 0) { foreach ($oneSvcSharePointSecStore in $svcSharePointSecStore) { [void] $outAccounts.Add((New-AccountInfo -kind 'spSecureStore' -login $null -domain $null -password $oneSvcSharePointSecStore.masterPwd -where (Get-MachineFQDN $oneSvcSharePointSecStore -doNotAssertFQDN))) } } if ((Get-CountSafe $accountsDtrAutologon) -gt 0) { foreach ($oneAccountDtrAutologon in $accountsDtrAutologon) { [void] $outAccounts.Add((New-AccountInfo -kind 'dtrAutlogon' -login $oneAccountDtrAutologon.login -domain $oneAccountDtrAutologon.domain -password $oneAccountDtrAutologon.pwd -where 'DTR')) } } if ((Get-CountSafe $fortifiedHosts) -gt 0) { foreach ($oneFortifiedHost in $fortifiedHosts) { foreach ($oneHostMachineId in $hostMachineIds) { $targettingApplied = $oneFortifiedHost.psbase.ParentNode.target.hostName #$hostAdminDomainUse = $localMachineMoniker $oneHostMachineIdUse = $oneHostMachineId if (Is-ValidString $targettingApplied) { #$hostAdminDomainUse = '{0} ({1})' -f $hostAdminDomainUse, $targettingApplied $oneHostMachineIdUse = '{0} ({1})' -f $oneHostMachineIdUse, $targettingApplied } [void] $outAccounts.Add((New-AccountInfo -kind 'hostFortification' -login $oneFortifiedHost.limitedUser -domain $null -password $oneFortifiedHost.limitedUserPwd -where $oneHostMachineIdUse)) } } } # # DBG ('Assert same passwords on domain accounts') [hashtable] $uniqueDomainAccounts = @{} foreach ($oneOutputAccount in $outAccounts) { $validationDomain = $oneOutputAccount.domain if (Is-EmptyString $validationDomain) { $validationDomain = $oneOutputAccount.where } [string] $validationLogin = $oneOutputAccount.login if (Is-EmptyString $validationLogin) { $validationLogin = $oneOutputAccount.kind } [string] $outputAccountKey = '<{0}>@{1}' -f $validationLogin, $validationDomain if ($uniqueDomainAccounts.Keys -contains $outputAccountKey) { DBGIF ('Different passwords on a single domain account: {0} | {1} | {2} != {3} | {4}' -f $uniqueDomainAccounts[$outputAccountKey].login, $uniqueDomainAccounts[$outputAccountKey].domain, $uniqueDomainAccounts[$outputAccountKey].pwd, $oneOutputAccount.pwd, $oneOutputAccount.where) { $uniqueDomainAccounts[$outputAccountKey].pwd -ne $oneOutputAccount.pwd } } else { [void] $uniqueDomainAccounts.Add($outputAccountKey, $oneOutputAccount) } } # # if (Is-ValidString $outputCSV) { DBG ('Saving the found accounts into CSV: {0}' -f $outputCSV) DBGSTART $outAccounts | Export-Csv -Path $outputCSV -Encoding UTF8 -Delimiter "`t" -NoTypeInformation -Force DBGER $MyInvocation.MyCommand.Name $error DBGEND } DBG ("Returning password info: -->`r`n{0}" -f ($outAccounts | ft -Auto | Out-String)) return $outAccounts } } function global:Pending-FileRenameOperation ([string] $what, [string] $where) { DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator)) <# DBGSTART Set-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Control\SessionManager -Name PendingFileRenameOperations -Value -Type DBGER $MyInvocation.MyCommand.Name $error DBGEND #> } function global:Delete-UnsafeOutput ([bool] $dontDoItActually = $false, [switch] $pendingDelete) { DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator)) [bool] $deleteUnsafeOutput = Parse-BoolSafe $xmlConfig.vmBuilder.deleteUnsafeOutput DBG ('Should we delete all unsafe output: {0}' -f $deleteUnsafeOutput) if ($deleteUnsafeOutput -and (-not $dontDoItActually)) { DBG ('Deleting unsafe expanded config file: {0} | pending = {1} | {2}' -f (Test-Path -Literal $global:expandedXmlConfigFile), $pendingDelete, $global:expandedXmlConfigFile) if (Test-Path -Literal $global:expandedXmlConfigFile) { if ($pendingDelete) { Pending-FileRenameOperation -what $global:expandedXmlConfigFile -where $null } else { DBGSTART Remove-Item $global:expandedXmlConfigFile -Force DBGER $MyInvocation.MyCommand.Name $error DBGEND } } DBG ('Deleting unsafe phase config file: {0} | pending = {1} | {2}' -f ((Is-ValidString $global:phaseCfgPath) -and (Test-Path -Literal $global:phaseCfgPath)), $pendingDelete, $global:phaseCfgPath) if ((Is-ValidString $global:phaseCfgPath) -and (Test-Path -Literal $global:phaseCfgPath)) { if ($pendingDelete) { Pending-FileRenameOperation -what $global:phaseCfgPath -where $null } else { DBGSTART Remove-Item $global:phaseCfgPath -Force DBGER $MyInvocation.MyCommand.Name $error DBGEND } } DBG ('Deleting unsafe output folder: {0} | pending = {1} | {2}' -f (Test-Path -Literal $global:outPath), $pendingDelete, $global:outPath) if (Test-Path -Literal $global:outPath) { if ($pendingDelete) { Pending-FileRenameOperation -what $global:outPath -where $null } else { DBGSTART Remove-Item $global:outPath -Recurse -Force DBGER $MyInvocation.MyCommand.Name $error DBGEND } } } return $deleteUnsafeOutput } function global:Fortify-EndpointMachine ([Parameter(Mandatory=$true)] [bool] $protectedHost, [string] $limitedUser, [string] $pwd, [string] $otherGroups) { DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator)) DBG ('Enable RDP and enforce RDP security') DBGSTART Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name 'SecurityLayer' -Value 2 Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name 'UserAuthentication' -Value 1 Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' -Name 'fDenyTSConnections' -Value 0 DBGER $MyInvocation.MyCommand.Name $error DBGEND DBG ('Close Windows Firewall as much as possible: {0}' -f (-not $protectedHost)) if (-not $protectedHost) { Run-Process 'NETSH' 'ADVFIREWALL SET allprofiles state on' Run-Process 'NETSH' 'ADVFIREWALL SET allprofiles firewallpolicy "blockinbound,blockoutbound"' Run-Process 'NETSH' 'ADVFIREWALL SET allprofiles settings inboundusernotification disable' Run-Process 'NETSH' 'ADVFIREWALL SET allprofiles settings remotemanagement disable' Run-Process 'NETSH' 'ADVFIREWALL SET allprofiles settings unicastresponsetomulticast enable' Run-Process 'NETSH' 'ADVFIREWALL FIREWALL SET RULE all NEW enable=no' Run-Process 'NETSH' 'ADVFIREWALL FIREWALL ADD RULE Name="Sevecek: RDP In (TCP 3389)" Dir=IN Action=Allow Enable=Yes Profile=Any Protocol=TCP LocalPort=3389 InterfaceType=LAN' Run-Process 'NETSH' 'ADVFIREWALL FIREWALL ADD RULE Name="Sevecek: Ping In (ICMPv4 type 8)" Dir=IN Action=Allow Enable=Yes Profile=Any Protocol=ICMPv4:8,any InterfaceType=LAN' Run-Process 'NETSH' 'ADVFIREWALL FIREWALL ADD RULE Name="Sevecek: DHCP Out (UDP 67)" Dir=OUT Action=Allow Enable=Yes Profile=Any Protocol=UDP RemotePort=67 InterfaceType=LAN' Run-Process 'NETSH' 'ADVFIREWALL FIREWALL ADD RULE Name="Sevecek: DNS Out (UDP 53)" Dir=OUT Action=Allow Enable=Yes Profile=Any Protocol=UDP RemotePort=53 InterfaceType=LAN' Run-Process 'NETSH' 'ADVFIREWALL FIREWALL ADD RULE Name="Sevecek: DNS Out (TCP 53)" Dir=OUT Action=Allow Enable=Yes Profile=Any Protocol=TCP RemotePort=53 InterfaceType=LAN' Run-Process 'NETSH' 'ADVFIREWALL FIREWALL ADD RULE Name="Sevecek: Ping Out (ICMPv4 type 8)" Dir=OUT Action=Allow Enable=Yes Profile=Any Protocol=ICMPv4:8,any InterfaceType=LAN' #Run-Process 'NETSH' 'ADVFIREWALL FIREWALL ADD RULE Name="Sevecek: RDP Out (TCP 3389)" Dir=OUT Action=Allow Enable=Yes Profile=Any Protocol=TCP RemotePort=3389 InterfaceType=LAN' $allowedHosts = @('eprezence.gopas.cz', 'outlook.com') if ((Get-CountSafe $allowedHosts) -gt 0) { foreach ($oneAllowedHost in $allowedHosts) { [Collections.ArrayList] $allowedHostIPs = @() if (Test-Dns $oneAllowedHost -resolvedIPs4 ([ref] $allowedHostIPs)) { if ((Get-CountSafe $allowedHostIPs) -gt 0) { foreach ($oneAllowedHostIP in $allowedHostIPs) { Run-Process 'NETSH' ('ADVFIREWALL FIREWALL ADD RULE Name="Sevecek: HTTP Out (TCP 80) ({0})" Dir=OUT Action=Allow Enable=Yes Profile=Any Protocol=TCP RemotePort=80 RemoteIp={1} InterfaceType=LAN' -f $oneAllowedHost, $oneAllowedHostIP) Run-Process 'NETSH' ('ADVFIREWALL FIREWALL ADD RULE Name="Sevecek: HTTP Out (TCP 443) ({0})" Dir=OUT Action=Allow Enable=Yes Profile=Any Protocol=TCP RemotePort=443 RemoteIp={1} InterfaceType=LAN' -f $oneAllowedHost, $oneAllowedHostIP) }} } else { DBGIF ('One allowed host cannot be found: {0}' -f $oneAllowedHost) { $true } } }} } # # DBG ('Get currently running user account') DBGSTART # Note: $false to get the Thread's access token in both cases, whether the thread is or is not impersonating $currentPrincipal = [Security.Principal.WindowsIdentity]::GetCurrent($false) DBGER $MyInvocation.MyCommand.Name $error DBGEND DBG ('Running under: {0} = {1}' -f $currentPrincipal.Name, $currentPrincipal.User.Value) [string] $currentUserSid = $currentPrincipal.User.Value DBG ('Disable all other local user accounts except for the current one: {0}' -f $currentUserSid) $localUsers = Get-WMIQueryArray '.' ('SELECT * FROM Win32_UserAccount WHERE Domain="{0}" AND SID <> "{1}"' -f $thisComputerNetBIOS, $currentUserSid) DBG ('Found local users other than the current one: #{0}' -f (Get-CountSafe $localUsers)) DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $localUsers) -lt 1 } if ((Get-CountSafe $localUsers) -ge 1) { foreach ($oneLocalUser in $localUsers) { DBG ('Disable one local user: {0} | {1}' -f $oneLocalUser.Name, (-not $protectedHost)) if (-not $protectedHost) { Disable-LocalUser $oneLocalUser.Name } } } # # DBG ('Disable any loopback violations') DBGSTART Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' -Name 'DisableLoopbackCheck' -Value 0 #Remove-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0' -Name BackConnectionHostNames DBGER $MyInvocation.MyCommand.Name $error DBGEND DBG ('Disable LM hashes') DBGSTART Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' -Name 'NoLMHash' -Value 1 DBGER $MyInvocation.MyCommand.Name $error DBGEND DBG ('Enforce NTLMv2') DBGSTART Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' -Name 'LmCompatibilityLevel' -Value 5 DBGER $MyInvocation.MyCommand.Name $error DBGEND DBG ('Disable IPv6') DBGSTART Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\TCPIP6\Parameters' -Name 'DisabledComponents' -Value 0xFF DBGER $MyInvocation.MyCommand.Name $error DBGEND DBG ('Disable SMBv1 and enforce SMB signing') DBGSTART if (Test-Path 'HKLM:\SYSTEM\CurrentControlSet\Services\mrxsmb10') { Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\mrxsmb10' -Name 'Start' -Value 4 } Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters' -Name 'SMB1' -Value 0 Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters' -Name 'EnableSecuritySignature' -Value 1 Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters' -Name 'RequireSecuritySignature' -Value 1 Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\LanmanWorkstation\Parameters' -Name 'EnableSecuritySignature' -Value 1 Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\LanmanWorkstation\Parameters' -Name 'RequireSecuritySignature' -Value 1 DBGER $MyInvocation.MyCommand.Name $error DBGEND DBG ('Reconfigure TLS') DBGSTART if (-not (Test-Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\PCT 1.0\Client')) { New-Item -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\PCT 1.0\Client' -Force | Out-Null } Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\PCT 1.0\Client' -Name Enabled -Value 0 if (-not (Test-Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\PCT 1.0\Server')) { New-Item -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\PCT 1.0\Server' -Force | Out-Null } Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\PCT 1.0\Server' -Name 'Enabled' -Value 0 if (-not (Test-Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 2.0\Client')) { New-Item -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 2.0\Client' -Force | Out-Null } Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 2.0\Client' -Name 'Enabled' -Value 0 if (-not (Test-Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 2.0\Server')) { New-Item -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 2.0\Server' -Force | Out-Null } Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 2.0\Server' -Name 'Enabled' -Value 0 if (-not (Test-Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 3.0\Client')) { New-Item -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 3.0\Client' -Force | Out-Null } Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 3.0\Client' -Name 'Enabled' -Value 0 if (-not (Test-Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 3.0\Server')) { New-Item -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 3.0\Server' -Force | Out-Null } Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 3.0\Server' -Name 'Enabled' -Value 0 if (-not (Test-Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Client')) { New-Item -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Client' -Force | Out-Null } # Note: some applications, such as EWS for example requires the functionality #Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Client' -Name 'Enabled' -Value 0 if (-not (Test-Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Server')) { New-Item -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Server' -Force | Out-Null } Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Server' -Name 'Enabled' -Value 0 if (-not (Test-Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Client')) { New-Item -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Client' -Force | Out-Null } Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Client' -Name 'Enabled' -Value 1 Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Client' -Name 'DisabledByDefault' -Value 0 if (-not (Test-Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Server')) { New-Item -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Server' -Force | Out-Null } Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Server' -Name 'Enabled' -Value 1 Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Server' -Name 'DisabledByDefault' -Value 0 if (-not (Test-Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Client')) { New-Item -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Client' -Force | Out-Null } Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Client' -Name 'Enabled' -Value 1 Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Client' -Name 'DisabledByDefault' -Value 0 if (-not (Test-Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server')) { New-Item -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server' -Force | Out-Null } Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server' -Name 'Enabled' -Value 1 Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server' -Name 'DisabledByDefault' -Value 0 DBGER $MyInvocation.MyCommand.Name $error DBGEND DBG ('Should we create the limited user: {0} | {1}' -f $limitedUser, (Is-ValidString $limitedUser)) if (Is-ValidString $limitedUser) { $userFlags = 0x10201 # ADS_UF_DONT_EXPIRE_PASSWD + ADS_UF_NORMAL_ACCOUNT + ADS_UF_SCRIPT Create-LocalObj 'user' $limitedUser $pwd $userFlags (Add-MultiValue 'Remote Desktop Users|Users' (Split-MultiValue $otherGroups) -unique $true) } DBG ('Enable UAC and enforce all confirmations: {0}' -f (-not $protectedHost)) if (-not $protectedHost) { DBGSTART Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -Name 'EnableLUA' -Value 1 Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -Name 'ConsentPromptBehaviorAdmin' -Value 4 Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -Name 'EnableVirtualization' -Value 1 Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -Name 'EnableInstallerDetection' -Value 1 Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -Name 'PromptOnSecureDesktop' -Value 1 Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -Name 'EnableSecureUIAPaths' -Value 1 Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -Name 'ValidateAdminCodeSignatures' -Value 0 Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -Name 'EnableUIADesktopToggle' -Value 0 Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -Name 'ConsentPromptBehaviorUser' -Value 3 DBGER $MyInvocation.MyCommand.Name $error DBGEND } [bool] $secureBootOk = $false DBGSTART; DBGEND; try { $secureBootOk = Confirm-SecureBootUEFI } catch { $error.Clear() } DBG ('Enable device guard / credentials guard: {0} | secureBoot = {1}' -f (-not $protectedHost), $secureBootOk) if ((-not $protectedHost) -and $secureBootOk) { # Note: msinfo32 - Device guard required security properties: +Base Virtualization Support $originalEnableVirtualizationBasedSecurity = Get-RegValue '.' 'SYSTEM\CurrentControlSet\Control\DeviceGuard' 'EnableVirtualizationBasedSecurity' 'Dword' $originalHypervisorEnforcedCodeIntegrity = Get-RegValue '.' 'SYSTEM\CurrentControlSet\Control\DeviceGuard' 'HypervisorEnforcedCodeIntegrity' 'Dword' # Note: msinfo32 - Device guard security services configured $originalLsaCfgFlags = Get-RegValue '.' 'SYSTEM\CurrentControlSet\Control\LSA' 'LsaCfgFlags' 'Dword' # Note: msinfo32 - Device guard required security properties: +SecureBoot (1), +DMAProtection (2), +SecureMemoryOverwrite (4), $originalRequirePlatformSecurityFeatures = Get-RegValue '.' 'SYSTEM\CurrentControlSet\Control\DeviceGuard' 'RequirePlatformSecurityFeatures' 'Dword' DBG ('Original DeviceGuard registry values: {0} | {1} | {2} | {3}' -f $originalEnableVirtualizationBasedSecurity, $originalHypervisorEnforcedCodeIntegrity, $originalLsaCfgFlags, $originalRequirePlatformSecurityFeatures) DBG ('Going to enforce Device Guard') DBGSTART # Note: the GPO values are of the same names but are all placed in policy key # HKLM\Software\Policies\Microsoft\Windows\DeviceGuard # which also includes the LsaCfgFlags value which is there as well Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\DeviceGuard' -Name 'EnableVirtualizationBasedSecurity' -Value 1 Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\DeviceGuard' -Name 'HypervisorEnforcedCodeIntegrity' -Value 0 Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\LSA' -Name 'LsaCfgFlags' -Value 2 # Note: DMA Protection and Secure Memory Overwrite is not available in VMs Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\DeviceGuard' -Name 'RequirePlatformSecurityFeatures' -Value 1 DBGER $MyInvocation.MyCommand.Name $error DBGEND #> } } function global:Fortify-Me () { DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator)) [bool] $fortified = $false [System.Xml.XmlElement] $fortifyConfig = $null if (Is-NonNull $global:vmConfig) { DBG ('Will use the VM configuration for foritification') if (Is-NonNull $global:vmConfig.fortifiedHost) { DBG ('The fortification config for this VM actually exists') $fortifyConfig = $global:vmConfig.fortifiedHost } } elseif (Is-NonNull $global:builderHostConfig) { DBG ('Will use the VMBUILDER host config for fortification') if (Is-NonNull $global:builderHostConfig.fortifiedHost) { DBG ('The fortification config for this VMBUILDER host actually exists') $fortifyConfig = $global:builderHostConfig.fortifiedHost } } if (Is-NonNull $fortifyConfig) { DBG ('Must fortify this machine: user = {0} | groups = {1}' -f $fortifyConfig.limitedUser, $fortifyConfig.otherGroups) # Note: must use the Parse-BoolSafe because the variable $global:isThisProtectedHost may not exist and the parameter transformation would fail for $null or '' Fortify-EndpointMachine -protectedHost (Parse-BoolSafe $global:isThisProtectedHost) -limitedUser $fortifyConfig.limitedUser -pwd $fortifyConfig.limitedUserPwd -otherGroups $fortifyConfig.otherGroups $fortified = $true } return $fortified } function global:Is-NodeTargettingThisBuilder ([System.Xml.XmlNode] $xmlNode, [bool] $ignoreTargetting) { $isDefault = Parse-BoolSafe $xmlNode.target.default $hit = (Is-EmptyString $xmlNode.target.hostName) -or ($global:thisComputerHost -like $xmlNode.target.hostName) -or ($ignoreTargetting -and $isDefault) DBG ('One targeted builder node: hit = {0} | host = {1} | wildcard = {2} | default = {3} | ignoring = {4}' -f $hit, $global:thisComputerHost, $xmlNode.target.hostName, $isDefault, $ignoreTargetting) return $hit } function global:Get-PrecompiledConfigFilePath ([string] $configId, [string] $customBuilderPath) { if (Is-EmptyString $customBuilderPath) { return (Join-Path $global:outPath (Join-Path $global:precompiledConfigFilesFolder ('{0}.xml' -f $configId))) } else { return (Join-Path (Join-Path $customBuilderPath (Join-Path $global:sevecekBuildupRoot $global:adlabOutputFolder)) (Join-Path $global:precompiledConfigFilesFolder ('{0}.xml' -f $configId))) } } function global:Precompile-Config ([string] $configId) { DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator)) [Xml.XmlElement] $outXmlConfig = $null if (Is-EmptyString $global:sourceXmlConfigFile) { $fromSourceXmlConfigFile = Join-Path $global:libCommonParentDir $global:libCommonDefaultXmlConfig } else { $fromSourceXmlConfigFile = $global:sourceXmlConfigFile } DBGIF ('Missing the source XML config file to precompile: {0}' -f $fromSourceXmlConfigFile) { -not (Test-Path -Literal $fromSourceXmlConfigFile) } if (Test-Path -Literal $fromSourceXmlConfigFile) { $onePrecompileConfigFile = Get-PrecompiledConfigFilePath $configId DBG ('Precompiling one configuration: {0} | {1} | {2}' -f $configId, $onePrecompileConfigFile, $fromSourceXmlConfigFile) [System.Xml.XmlDocument] $precompiledXmlFullConfig = $null $precompilationOk = LoadExpand-XmlConfig -inXML $fromSourceXmlConfigFile -chooseConfig $configId -refFullXmlConfig ([ref] $precompiledXmlFullConfig) -refXmlConfig ([ref] $outXmlConfig) DBGIF $MyInvocation.MyCommand.Name { -not $precompilationOk } if ($precompilationOk) { if (Test-Path -Literal $onePrecompileConfigFile) { DBG ('The precompiled XML config file already exists. Delete: {0}' -f $onePrecompileConfigFile) DBGSTART Remove-Item $onePrecompileConfigFile -Force DBGER $MyInvocation.MyCommand.Name $error DBGEND } DBG ('Precreate the precompiled XML config file: {0}' -f $onePrecompileConfigFile) DBGSTART New-Item $onePrecompileConfigFile -ItemType File -Force | Out-Null DBGER $MyInvocation.MyCommand.Name $error DBGEND DBG ('Save the precompiled XML: {0}' -f $onePrecompileConfigFile) DBGSTART $precompiledXmlFullConfig.Save($onePrecompileConfigFile) DBGER $MyInvocation.MyCommand.Name $error DBGEND } } return $outXmlConfig } function global:Start-NestedBuilder () { DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator)) $res = $global:VM_STATUS_FAIL DBGIF 'Should not BUILD any VMs' { -not ((Is-ValidString $vmConfig.builder.build) -and (-not (Parse-BoolSafe $vmConfig.builder.dnd))) } if ((Is-ValidString $vmConfig.builder.build) -and (-not (Parse-BoolSafe $vmConfig.builder.dnd))) { $nestedBuilderTarget = Resolve-VolumePath $vmConfig.builder.target $nestedBuilderFolder = Join-Path (Find-MarkedVolume $global:builderVhdxMarker) $global:buildupFolderNameOnVhdx $nestedBuilderBAT = Join-Path $nestedBuilderFolder 'build-machines-choose.bat' DBG ('Going to run BUILDER: {0} | {1}' -f $vmConfig.builder.build, $nestedBuilderTarget, $nestedBuilderBAT) DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path -Literal (Split-Path -Qualifier $nestedBuilderTarget)) } DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path -Literal $nestedBuilderBAT) } DBG ('Create the folder to host the machines: {0}' -f $nestedBuilderTarget) DBGSTART New-Item -Path $nestedBuilderTarget -ItemType Directory -Force | Out-Null DBGER $MyInvocation.MyCommand.Name $error DBGEND [string] $otherNestedBuilderParams = $null if (Parse-BoolSafe $vmConfig.builder.precompiledConfig) { $foundPrecompiledConfigFile = Get-PrecompiledConfigFilePath $vmConfig.builder.build -customBuilderPath $nestedBuilderFolder DBGIF ('Precompiled config file does not exist: {0}' -f $foundPrecompiledConfigFile) { -not (Test-Path -Literal $foundPrecompiledConfigFile) } if (Test-Path -Literal $foundPrecompiledConfigFile) { DBG ('Will use the precompiled config file: {0}' -f $foundPrecompiledConfigFile) $otherNestedBuilderParams = '{0} "-configFile\:{1}"' -f $otherNestedBuilderParams, $foundPrecompiledConfigFile } } if ((Test-Path -Literal $nestedBuilderTarget) -and (Test-Path -Literal $nestedBuilderBAT)) { $returnValue = Run-Process $nestedBuilderBAT ('{0} {1} "-machineRoot\:{2}" -outsideRestarts' -f $vmConfig.builder.build, $otherNestedBuilderParams.Trim(), $nestedBuilderTarget) -returnExitCode $true -newWindow -showWindow -doNotRedirOut $true DBG ('Nested BUILDER return status: {0}' -f $returnValue) DBGIF ('Nested BUILDER operation failed: {0}' -f $returnValue) { ($returnValue -gt 0) -and ($returnValue -le 1000) } DBGIF ('Cannot let nested BUILDER rerun from a new VHDX location') { $returnValue -eq 1002 } DBGIF ('Unknown nested BUILDER return status: {0}' -f $returnValue) { $returnValue -gt 1003 } if ($returnValue -eq 0) { $res = $global:VM_STATUS_OK } elseif ($returnValue -eq 1001) { $res = $global:VM_RECYCLE_REQUIRED } elseif ($returnValue -eq 1003) { $res = $global:VM_FINISHED_RESTART_REQUIRED } } } DBG ('Nested BUILDER final result: {0}' -f $res) return $res } function global:Copy-DeployContent ([string] $src, [string] $target, [string] $replace) { DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator)) DBGIF $MyInvocation.MyCommand.Name { (Is-EmptyString $src) -or (Is-EmptyString $target) } if ((Is-ValidString $src) -and (Is-ValidString $target)) { $src = Resolve-VolumePath $src $target = Resolve-VolumePath $target if (-not (Test-Path -Literal $target)) { DBG ('The target folder does not exist, create: {0}' -f $target) DBGSTART New-Item -Path $target -ItemType Directory -Force | Out-Null DBGER $MyInvocation.MyCommand.Name $error DBGEND } DBG ('Copying one: src = {0} | target = {1}' -f $src, $target) DBGSTART # Note: we cannot join with *.* because it would not copy the subfolders # this way using only the * it works well according to my testing # Note: the target must exist as a folder or the tool copies all the individual files # into just the single file with the target name Copy-Item -Path (Join-Path $src '*') -Destination $target -Recurse -Force DBGER $MyInvocation.MyCommand.Name $error DBGEND DBG ('Any argument replacements: {0} | {1}' -f (Is-ValidString $replace), $replace) if (Is-ValidString $replace) { DBG ('Get the files to try argument replacments: {0}' -f $target) DBGSTART [IO.FileInfo[]] $filesToReplaceArguments = Get-ChildItem $target -Force | ? { -not $_.PsIsContainer} | select -Expand FullName DBGER $MyInvocation.MyCommand.Name $error DBGEND if ($filesToReplaceArguments.Length -gt 0) { foreach ($oneFileToReplaceArguments in $filesToReplaceArguments) { DBG ('One file to replace tokens: {0}' -f $oneFileToReplaceArguments) Replace-ArgumentsInFile -sourceFilePath $oneFileToReplaceArguments -arguments $replace -outFilePath $oneFileToReplaceArguments -encoding UTF8 }} } } } function global:Update-ScheduledTasksPreference ( [string] $domain, [string] $gpoName, [string] $userOrMachine = 'machine', [string] $cmdReplacements, [string] $shareName, [string] $shareSource ) { DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator)) DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $domain } DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $gpoName } DBGIF $MyInvocation.MyCommand.Name { ($userOrMachine -ne 'user') -and ($userOrMachine -ne 'machine') } DBG ('First get the existing GPO') $gpo = Open-GPO $gpoName $domain DBGIF $MyInvocation.MyCommand.Name { Is-Null $gpo } if (Is-NonNull $gpo) { [string] $gpoSysvolPath = '\\{0}\SYSVOL\{0}\Policies\{1}\{2}' -f $domain, $gpo.ID, $userOrMachine DBGIF ('GPO does not exist in SYSVOL: {0}' -f $gpoSysvolPath) { -not (Test-Path -Literal $gpoSysvolPath) } if (Test-Path -Literal $gpoSysvolPath) { $schdXmlPath = Join-Path $gpoSysvolPath 'Preferences\ScheduledTasks\ScheduledTasks.xml' DBG ('The preference xml should be found at path: {0}' -f $schdXmlPath) [XML] $schdXml = $null DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path -Literal $schdXmlPath) } if (Test-Path -Literal $schdXmlPath) { DBG ('The scheduled tasks file exists. Load it') DBGSTART $schdXml = [XML] (cat $schdXmlPath) DBGER $MyInvocation.MyCommand.Name $error DBGEND DBGIF $MyInvocation.MyCommand.Name { Is-Null $schdXml } if (Is-NonNull $schdXml) { DBG ('Going to update the task: {0}' -f $schdXml.ScheduledTasks.TaskV2.Properties.name) [string] $command = $schdXml.ScheduledTasks.TaskV2.Properties.Task.Actions.Exec.Command DBG ('Going to update the command: {0}' -f $command) DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $schdXml.ScheduledTasks.TaskV2.Properties.name } DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $command } if ((Is-ValidString $schdXml.ScheduledTasks.TaskV2.Properties.name) -and (Is-ValidString $command)) { DBG ('Will replace the tokens with the following replacement strings: {0}' -f $cmdReplacements) [string[]] $replacements = Split-MultiValue $cmdReplacements DBGIF $MyInvocation.MyCommand.Name { $replacements.Length -lt 1 } foreach ($oneReplacement in $replacements) { [string] $oneToken = '${0}$' -f $oneReplacement.Substring(0, $oneReplacement.IndexOf('$')) [string] $oneValue = $oneReplacement.Substring(($oneReplacement.IndexOf('$') + 1)) DBG ('Replacement to perform: what = {0} | with = {1}' -f $oneToken, $oneValue) DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $oneToken } DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $oneValue } $command = $command.Replace($oneToken, $oneValue) } DBG ('Updating the command: {0}' -f $command) DBGSTART $schdXml.ScheduledTasks.TaskV2.Properties.Task.Actions.Exec.Command = [string] $command DBGER $MyInvocation.MyCommand.Name $error DBGEND DBG ('Save the new xml into the original file: {0}' -f $schdXmlPath) DBGSTART [void] (New-Item $schdXmlPath -Force -Type File) $schdXml.Save($schdXmlPath) DBGER $MyInvocation.MyCommand.Name $error DBGEND DBG ('Should we install some task binaries: {0} | {1}' -f $shareName, $shareSource) if ((Is-ValidString $shareName) -and (Is-ValidString $shareSource)) { [string] $shareBase = ('\\{0}\NETLOGON' -f $domain) [string] $sharePath = Join-Path $shareBase $shareName DBG ('Creating the share folder: {0} | {1}' -f $shareBase, $sharePath) DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path -Literal $shareBase) } DBGIF $MyInvocation.MyCommand.Name { Test-Path -Literal $sharePath } if (-not (Test-Path -Literal $sharePath)) { Copy-DeployContent -src $shareSource -target $sharePath } } } } } } } } function global:New-GPORegistryPreference ( [string] $domain, [string] $gpoName, [string] $userOrMachine = 'machine', [string] $hive = 'hklm', [string] $key, [string] $valueName, [string] $value, [string] $regType = 'dword', [bool] $valueInHex = $false, [char] $action = 'R', [bool] $shouldRemove = $true ) { DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator)) DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $domain } DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $gpoName } DBGIF $MyInvocation.MyCommand.Name { ($userOrMachine -ne 'user') -and ($userOrMachine -ne 'machine') } DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $hive } DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $key } DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $regType } <# -domain 'gopas.virtual' -gpoName 'Ease: IE Local Intranet Defaults' -userOrMachine 'user' -hive 'hkcu' -key 'Software\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap\Domains\gopas.cz' -valueName '*' -value 1 -regType 'dword' -action 'R' -shouldRemove $true #> DBG ('First get the existing GPO') $gpo = Open-GPO $gpoName $domain DBGIF $MyInvocation.MyCommand.Name { Is-Null $gpo } if (Is-NonNull $gpo) { [string] $gpoSysvolPath = '\\{0}\SYSVOL\{0}\Policies\{1}\{2}' -f $domain, $gpo.ID, $userOrMachine DBGIF ('GPO does not exist in SYSVOL: {0}' -f $gpoSysvolPath) { -not (Test-Path -Literal $gpoSysvolPath) } if (Test-Path -Literal $gpoSysvolPath) { $regXmlPath = Join-Path $gpoSysvolPath 'Preferences\Registry\Registry.xml' DBG ('The preference xml should be found at path: {0}' -f $regXmlPath) [XML] $regXml = $null if (Test-Path -Literal $regXmlPath) { DBG ('The registry file exists. Load it') DBGSTART $regXml = [XML] (cat $regXmlPath) DBGER $MyInvocation.MyCommand.Name $error DBGEND } else { DBG ('The registry file does not exist. Will create new') } $newRegXml = New-PreferenceRegistryItem -hive $hive -key $key -valueName $valueName -value $value -regType $regType -action $action -shouldRemove $shouldRemove -xml $regXml DBG ('Save the resulting XML: {0}' -f $regXmlPath) DBGSTART [void] (New-Item $regXmlPath -Force -Type File) $newRegXml.Save($regXmlPath) DBGER $MyInvocation.MyCommand.Name $error DBGEND [string] $ldapExtensionAttr = 'gPC{0}ExtensionNames' -f $userOrMachine DBG ('Going to update the AD extension attribute: {0} | {1}' -f $ldapExtensionAttr, $gpo.Path) [Collections.ArrayList] $deList = @() $gpoDE = Get-DE $gpo.Path ([ref] $deList) [string] $extensionsStr = GDES $gpoDE $ldapExtensionAttr DBG ('Current LDAP extension attribute value: {0}' -f $extensionsStr) [string] $regItemZeros = '{00000000-0000-0000-0000-000000000000}' [string] $regItemOne = '{BEE07A6A-EC9F-4659-B8C9-0B1937907C83}' [string] $regItemBoth = '{B087BE9D-ED37-454F-AF9C-04291E351182}{BEE07A6A-EC9F-4659-B8C9-0B1937907C83}' if (Is-ValidString $extensionsStr) { [string[]] $extensions = $extensionsStr.Split('[]') | ? { Is-ValidString $_.Trim() } DBG ('Extension tokens found: #{0} | {1}' -f $extensions.Length, ($extensions -join ',')) DBGIF $MyInvocation.MyCommand.Name { $extensions.Length -lt 2 } DBGIF $MyInvocation.MyCommand.Name { $extensions[0] -notlike "$regItemZeros*" } if ($extensions[0] -notlike "*$regItemOne*") { DBG ('Registry extension not enabled. Adding') $extensions[0] = $extensions[0] + $regItemOne $extensionsStr = '' foreach ($oneExtension in $extensions) { $extensionsStr += '[{0}]' -f $oneExtension } $extensionsStr += '[{0}]' -f $regItemBoth } else { DBGIF $MyInvocation.MyCommand.Name { $extensionsStr -notlike "*$regItemBoth*" } } } else { DBG ('No extensions enabled yet, enabling the registry') $extensionsStr = '[{0}{1}][{2}]' -f $regItemZeros, $regItemOne, $regItemBoth } DBG ('Setting extension string back to AD: {0}' -f $extensionsStr) DBGSTART $gpoDE.Put($ldapExtensionAttr, $extensionsStr) $adsiRs = $gpoDE.SetInfo() DBGER $MyInvocation.MyCommand.Name $error DBGEND DBG ('ADSI Result: {0}' -f ($adsiRs | Out-String)) [int] $gpoVersion = [int](GDES $gpoDE versionNumber) [int] $gpoVersionNew = (([int]($gpoVersion / 0x10000)) + 1) * 0x10000 + (($gpoVersion % 0x10000) + 1) DBG ('Updating GPO version: {0}' -f $gpoVersion, $gpoVersionNew) DBGSTART $gpoDE.Put('versionNumber', $gpoVersionNew) $adsiRs = $gpoDE.SetInfo() DBGER $MyInvocation.MyCommand.Name $error DBGEND DBG ('ADSI Result: {0}' -f ($adsiRs | Out-String)) Dispose-List ([ref] $deList) } } } function global:Create-RdpFiles ([hashtable] $namedTargets, [string] $folder, [switch] $entrust, [string] $username, [switch] $enableInFW, [string] $password) { DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator)) $templateRDP = @' username:s:{0} full address:s:{1} screen mode id:i:2 session bpp:i:32 compression:i:1 keyboardhook:i:2 audiocapturemode:i:0 videoplaybackmode:i:1 connection type:i:4 networkautodetect:i:0 bandwidthautodetect:i:1 displayconnectionbar:i:1 disable wallpaper:i:1 allow font smoothing:i:1 allow desktop composition:i:1 disable full window drag:i:1 disable menu anims:i:1 disable themes:i:0 disable cursor setting:i:0 bitmapcachepersistenable:i:1 audiomode:i:2 redirectprinters:i:0 redirectcomports:i:0 redirectsmartcards:i:0 redirectclipboard:i:1 redirectposdevices:i:0 autoreconnection enabled:i:1 authentication level:i:0 negotiate security layer:i:1 remoteapplicationmode:i:0 gatewayusagemethod:i:0 '@ DBGIF $MyInvocation.MyCommand.Name { $namedTargets.Count -lt 1 } if (Is-EmptyString $folder) { $folder = (Get-Location).Path } if ($namedTargets.Count -ge 1) { foreach ($oneNamedTarget in $namedTargets.Keys) { DBG ('Creating RDP files for target: {0} | {1}' -f $oneNamedTarget, ($namedTargets[$oneNamedTarget] -join ',')) foreach ($oneTarget in $namedTargets[$oneNamedTarget]) { $rdpFilePath = Join-Path $folder ('{0} ({1}).rdp' -f $oneNamedTarget, $oneTarget) DBG ('Creating one RDP file for target: {0} | {1}' -f $oneNamedTarget, $oneTarget, $rdpFilePath) ($templateRDP -f $username, $oneTarget) | Out-File $rdpFilePath -Encoding UTF8 -Force # Note: this method does not allow defining RD Gateway parameters #DBGSTART #$shell = New-Object -Com WScript.Shell #$shortcut = $shell.CreateShortcut($rdpFilePath) #$shortcut.TargetPath = 'mstsc' #$shortcut.Arguments = '/f /v:{0} /noConsentPrompt' -f $oneTarget #$shortcut.Save() #DBGER $MyInvocation.MyCommand.Name $error #DBGEND if ($entrust) { DBG ('Entrusting the requested RDP target: {0}' -f $oneTarget) $noPromptHostsReg = 'HKCU:\Software\Microsoft\Terminal Server Client\LocalDevices' if (-not (Test-Path $noPromptHostsReg)) { New-Item $noPromptHostsReg -ItemType Key -Force | Out-Null } Set-ItemProperty $noPromptHostsReg -Name $oneTarget -Value 13 -Force } if (Is-ValidString $password) { Run-Process cmdkey ('"/add:TERMSRV/{0}" "/user:{1}" "/pass:{2}"' -f $oneTarget, $username, $password) } if ($enableInFW) { [Collections.ArrayList] $resolvedOneTargetIPs = @() DBG ('Is the target specified directly as an IP address: {0} | {1}' -f (Is-IPv4OrIPv6Address $oneTarget), $oneTarget) if (-not (Is-IPv4OrIPv6Address $oneTarget)) { $ipResolutionResult = Test-Dns -address $oneTarget -resolvedIPs4 ([ref] $resolvedOneTargetIPs) DBGIF $MyInvocation.MyCommand.Name { -not $ipResolutionResult } } else { [void] $resolvedOneTargetIPs.Add($oneTarget) } if ((Get-CountSafe $resolvedOneTargetIPs) -gt 0) { foreach ($oneTargetIP in $resolvedOneTargetIPs) { Run-Process 'NETSH' ('ADVFIREWALL FIREWALL ADD RULE Name="Sevecek: RDP Out (TCP 3389) ({0})" Dir=OUT Action=Allow Enable=Yes Profile=Any Protocol=TCP RemotePort=3389 RemoteIp={1} InterfaceType=LAN' -f $oneNamedTarget, $oneTargetIP) }} } } }} } function global:Get-BuildupRequiredDisks ([Xml.XmlElement] $xmlConfigToUse) { DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator)) DBGIF $MyInvocation.MyCommand.Name { Is-Null $xmlConfigToUse } [Collections.ArrayList] $disks = @() if (Is-NonNull $xmlConfigToUse) { $vmsDefined = $xmlConfigToUse.SelectNodes('./VMs/MACHINE') if ((Get-CountSafe $vmsDefined) -gt 0) { $vmDiskElements = $xmlConfigToUse.SelectNodes('./VMs/MACHINE/vm') DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $vmDiskElements) -lt 1 } if ((Get-CountSafe $vmDiskElements) -ge 1) { foreach ($oneVmDiskElement in $vmDiskElements) { [string[]] $vhds = Split-MultiValue $oneVmDiskElement.vhd [string[]] $isos = Split-MultiValue $oneVmDiskElement.iso if ($vhds.Length -gt 0) { foreach ($oneVhdOrIso in ($vhds + $isos)) { $oneVhdOrIso = Strip-ValueFlags $oneVhdOrIso if ($oneVhdOrIso -ne '-') { $oneVhdOrIso = Resolve-VolumePath $oneVhdOrIso if (Is-ValidString $oneVhdOrIso) { $oneVhdOrIso = Resolve-PathSafe $oneVhdOrIso DBGIF ('Weird disk path obtained: {0}' -f $oneVhdOrIso) { (Is-EmptyString $oneVhdOrIso) -or (($oneVhdOrIso -notlike '*.vhd') -and ($oneVhdOrIso -notlike '*.vhdx') -and ($oneVhdOrIso -notlike '*.iso')) } if (-not ((Is-EmptyString $oneVhdOrIso) -or (($oneVhdOrIso -notlike '*.vhd') -and ($oneVhdOrIso -notlike '*.vhdx') -and ($oneVhdOrIso -notlike '*.iso')))) { DBG ('One explicit VHD obtained from the config: {0}' -f $oneVhdOrIso) if (-not (Contains-Safe $disks $oneVhdOrIso)) { [void] $disks.Add($oneVhdOrIso) } } } } } }} }} } DBG ("Returning the following required disks: {0} | -->`r`n{1}" -f $disks.Count, ($disks | Out-String)) return $disks } function global:Is-OracleRemediationInstalled () { [bool] $isInstalled = $false [hashtable] $oracleVersions = @{ 6.1007601 = [Version] '6.1.7601.24117' 6.2009200 = [Version] '6.2.9200.22432' 6.3009600 = [Version] '6.3.9600.18999' 10.0014393 = [Version] '10.0.14393.2248' 10.0015063 = [Version] '10.0.15063.1088' 10.0016299 = [Version] '10.0.16299.431' } [string] $tspkg = Join-Path $env:SystemRoot System32\tspkg.dll if (Test-Path $tspkg) { [System.IO.FileInfo] $tspkgItem = Get-Item $tspkg [string] $tspkgVersionStr = ([regex]::Match($tspkgItem.VersionInfo.FileVersion, '\A\d+\.\d+\.\d+\.\d+')).Value [Version] $tspkgVersion = [Version] $tspkgVersionStr } if ($global:thisOSVersionNumberWithBuild -gt 10.0016299) { $isInstalled = $true } else { if ($oracleVersions.ContainsKey($global:thisOSVersionNumberWithBuild)) { if ($oracleVersions[$global:thisOSVersionNumberWithBuild] -le $tspkgVersion) { $isInstalled = $true } } } DBG ('The TSPKG CredSSP oracle remediation is installed: os = {0} | required = {1} | path = {2} | verStr = {3} | ver = {4} | isInstalled = {5}' -f $global:thisOSVersionNumberWithBuild, $oracleVersions[$global:thisOSVersionNumberWithBuild], $tspkgItem.FullName, $tspkgVersionStr, $tspkgVersion, $isInstalled) return $isInstalled } function global:Repair-PsReadLineMissing () { DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator)) [string] $ourPsReadline = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\Modules\PSReadLine' [bool] $psreadline = Test-Path -Literal $env:ProgramFiles\WindowsPowerShell\Modules\PSReadLine [bool] $psreadline32 = (($global:thisOSArchitecture -eq '32-bit') -and $psreadline) -or (($global:thisOSArchitecture -ne '32-bit') -and (Test-Path -Literal ${env:ProgramFiles(x86)}\WindowsPowerShell\Modules\PSReadLine)) [bool] $psreadlineSys = Test-Path -Literal $ourPsReadline [bool] $psreadlineByUs = Test-Path -Literal (Join-Path $ourPsReadline psreadline.sevecek.repair) DBGIF ('Weird combination of PsReadLine modules installed: default = {0} | x32 = {1} | sys = {2} | us = {3}' -f $psreadline, $psreadline32, $psreadlineSys, $psreadlineByUs) { ($psreadline -xor $psreadline32) -or ($psreadlineSys -and (-not $psreadlineByUs)) } if ((-not $psreadline) -and (-not $psreadline32) -and (-not $psreadlineSys)) { DBG ('Disable PsReadLine default loading on system with PsReadLine missing') DBGSTART New-Item -Path $ourPsReadline -ItemType Directory -Force | Out-Null New-Item -Path (Join-Path $ourPsReadline psreadline.psm1) -ItemType File -Force | Out-Null New-Item -Path (Join-Path $ourPsReadline psreadline.sevecek.repair) -ItemType File -Force | Out-Null DBGER $MyInvocation.MyCommand.Name $error DBGEND } } function global:Install-FTPSite ([System.Xml.XmlElement] $ftpsXml) { DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator)) DBG ('Going to install FTPS site: {0} | {1}' -f $ftpsXml.instance, $ftpsXml.site) DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $ftpsXml.instance } DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $ftpsXml.site } if (Is-ValidString $ftpsXml.instance) { [string] $ftpCert = Enroll-AppCertificate $ftpsXml.cert.one DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $ftpCert } $ftpBinding = 'ftp://*:21' $ftpBaseRoot = Resolve-VolumePath $ftpsXml.root $ftpRoot = Join-Path $ftpBaseRoot $ftpsXml.instance if (-not (Test-Path -Literal $ftpBaseRoot)) { Create-NTFSFolderAndShare $ftpBaseRoot $null 'F$Administrators|RT$Authenticated Users' $null } $ftpRootPermissions = '' [Collections.ArrayList] $ftpReaders = @() [Collections.ArrayList] $ftpWriters = @() [Collections.ArrayList] $ftpOnlyWriters = @() if (Is-ValidString $ftpsXml.readers) { DBG ('Some readers requested: {0}' -f $ftpsXml.readers) foreach ($oneFtpReader in (Split-MultiValue $ftpsXml.readers)) { $oneFtpReaderSAM = Get-SAMLogin $oneFtpReader $oneFtpReaderSID = Assert-AccountExists $oneFtpReaderSAM -returnSID $true if (Is-ValidString $oneFtpReaderSID) { $ftpRootPermissions = Add-MultiValue $ftpRootPermissions ('R${0}' -f $oneFtpReaderSAM) $true [void] $ftpReaders.Add($oneFtpReaderSAM) } } } if (Is-ValidString $ftpsXml.writers) { DBG ('Some writers requested: {0}' -f $ftpsXml.writers) foreach ($oneFtpWriter in (Split-MultiValue $ftpsXml.writers)) { $oneFtpWriterSAM = Get-SAMLogin $oneFtpWriter $oneFtpWriterSID = Assert-AccountExists $oneFtpWriterSAM -returnSID $true if (Is-ValidString $oneFtpWriterSID) { $ftpRootPermissions = Add-MultiValue $ftpRootPermissions ('M${0}' -f $oneFtpWriterSAM) $true [void] $ftpWriters.Add($oneFtpWriterSAM) } } } if (Is-ValidString $ftpsXml.onlyWriters) { DBG ('Some only-writers requested: {0}' -f $ftpsXml.onlyWriters) foreach ($oneFtpOnlyWriter in (Split-MultiValue $ftpsXml.onlyWriters)) { $oneFtpOnlyWriterSAM = Get-SAMLogin $oneFtpOnlyWriter $oneFtpOnlyWriterSID = Assert-AccountExists $oneFtpOnlyWriterSAM -returnSID $true if (Is-ValidString $oneFtpOnlyWriterSID) { $ftpRootPermissions = Add-MultiValue $ftpRootPermissions ('W${0}' -f $oneFtpOnlyWriterSAM) $true [void] $ftpOnlyWriters.Add($oneFtpOnlyWriterSAM) } } } Create-NTFSFolderAndShare $ftpRoot $null $ftpRootPermissions $null $false Install-WindowsFeaturesUniversal @('Web-Ftp-Service', 'Web-Mgmt-Console') $false $global:installMediaVolume $global:installISOVolume $appCmd = Join-Path $env:SystemRoot 'system32\inetsrv\appcmd.exe' Run-Process $appCmd ('add site /name:"{0}" /bindings:"{1}" /physicalpath:"{2}"' -f $ftpsXml.site, $ftpBinding, $ftpRoot) Run-Process $appCmd ('set config /section:system.applicationHost/sites /[name=''"{0}"''].ftpServer.security.ssl.controlChannelPolicy:"SslRequire" /commit:apphost' -f $ftpsXml.site) Run-Process $appCmd ('set config /section:system.applicationHost/sites /[name=''"{0}"''].ftpServer.security.ssl.dataChannelPolicy:"SslRequire" /commit:apphost' -f $ftpsXml.site) Run-Process $appCmd ('set config /section:system.applicationHost/sites /[name=''"{0}"''].ftpServer.security.ssl.serverCertHash:"{1}" /commit:apphost' -f $ftpsXml.site, $ftpCert) DBG ('Enable FTP logging or not: {0}' -f (Is-ValidString $ftpsXml.logDir)) if (Is-ValidString $ftpsXml.logDir) { $ftpLogDir = Resolve-VolumePath $ftpsXml.logDir if (-not (Test-Path -Literal $ftpLogDir)) { DBG ('Creating the FTP logging base dir: {0}' -f $ftpLogDir) Create-NTFSFolderAndShare $ftpLogDir $null 'F$Administrators' $null } Run-Process $appCmd ('set config /section:system.applicationHost/sites /[name=''"{0}"''].ftpServer.logFile.directory:"{1}" /commit:apphost' -f $ftpsXml.site, $ftpLogDir) Run-Process $appCmd ('set config -section:system.applicationHost/sites /[name=''"{0}"''].ftpServer.logFile.enabled:"True" /commit:apphost' -f $ftpsXml.site) Run-Process $appCmd ('set config -section:system.applicationHost/sites /[name=''"{0}"''].ftpServer.logFile.logExtFileFlags:"Date,Time,ClientIP,ClientPort,Host,UserName,ServerIP,Method,UriStem,FtpStatus,Win32Status,FtpSubStatus,ServerPort,Session,FullPath,BytesRecv,BytesSent,TimeTaken" /commit:apphost' -f $ftpsXml.site) } else { Run-Process $appCmd ('set config -section:system.applicationHost/sites /[name=''"{0}"''].ftpServer.logFile.enabled:"False" /commit:apphost' -f $ftpsXml.site) } Run-Process $appCmd ('set config /section:system.applicationHost/sites /[name=''"{0}"''].ftpServer.security.authentication.basicAuthentication.enabled:true /commit:apphost' -f $ftpsXml.site) DBG ('Applying any FTP reader authorization rules: #{0} | {1}' -f (Get-CountSafe $ftpReaders), ($ftpReaders -join ',')) if ((Get-CountSafe $ftpReaders) -gt 0) { foreach ($oneFtpReader in $ftpReaders) { Run-Process $appCmd ('set config "{0}" /section:system.ftpserver/security/authorization /+[accessType=''Allow'',permissions=''Read'',roles=''"{1}"'',users=''''] /commit:apphost' -f $ftpsXml.site, $oneFtpReader) } } DBG ('Applying any FTP writer authorization rules: #{0} | {1}' -f (Get-CountSafe $ftpWriters), ($ftpWriters -join ',')) if ((Get-CountSafe $ftpWriters) -gt 0) { foreach ($oneFtpWriter in $ftpWriters) { Run-Process $appCmd ('set config "{0}" /section:system.ftpserver/security/authorization /+[accessType=''Allow'',permissions=''Read,Write'',roles=''"{1}"'',users=''''] /commit:apphost' -f $ftpsXml.site, $oneFtpWriter) } } DBG ('Applying any FTP only-writer authorization rules: #{0} | {1}' -f (Get-CountSafe $ftpOnlyWriters), ($ftpOnlyWriters -join ',')) if ((Get-CountSafe $ftpOnlyWriters) -gt 0) { foreach ($oneFtpOnlyWriter in $ftpOnlyWriters) { Run-Process $appCmd ('set config "{0}" /section:system.ftpserver/security/authorization /+[accessType=''Allow'',permissions=''Write'',roles=''"{1}"'',users=''''] /commit:apphost' -f $ftpsXml.site, $oneFtpOnlyWriter) } } # # if ($global:thisOSVersionNumber -lt 10) { Install-Update -kb 2888853 } } } # SIG # Begin signature block # MIIYMAYJKoZIhvcNAQcCoIIYITCCGB0CAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCLoTEfe9AggQs7 # ptwKh1STTdVlqIN2MtnW6cYP91rJjaCCE0cwggYEMIID7KADAgECAgoqHIRwAAEA # AAB/MA0GCSqGSIb3DQEBCwUAMGwxCzAJBgNVBAYTAkNaMRcwFQYDVQQIEw5DemVj # aCBSZXB1YmxpYzENMAsGA1UEBxMEQnJubzEQMA4GA1UEChMHU2V2ZWNlazEjMCEG # A1UEAxMaU2V2ZWNlayBFbnRlcnByaXNlIFJvb3QgQ0EwHhcNMTkwNjExMTkyMzMy # WhcNMjQwNjA5MTkyMzMyWjCBjzELMAkGA1UEBhMCQ1oxFzAVBgNVBAgTDkN6ZWNo # IFJlcHVibGljMQ0wCwYDVQQHEwRCcm5vMRwwGgYDVQQKExNJbmcuIE9uZHJlaiBT # ZXZlY2VrMRcwFQYDVQQDEw5PbmRyZWogU2V2ZWNlazEhMB8GCSqGSIb3DQEJARYS # b25kcmVqQHNldmVjZWsuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC # AQEAnkjWNkK4FfUUN8iAN91ry+wsSn8cFKJbMnROAqTrx8t3H315p2/bUG2DosCF # Odu0WcaTOLdm5obhT+/3O7BqpdcnlWKlSEz4AL9zQeCbe4++NObBVPBbPE16j9C4 # xELoXW/Ti86C2PEkN5azGUvxGxzQQ45g32OsEI+Bh05qHMkk3oQ6L8O0Fpd5W4e+ # L4HuKS3JOikNhhryTNPD9grF/0wXTzn94TrL1GohuaCPh8g9HOtMoDCd+ExnqV8q # 4k60D37BOK1I81hYFIBn8MvCsjMRC5TK87MtI7aUUIeve5kopc8ZpxNti3F/+Puh # 4UUxL3nKjfAM6HE0b7FqkfkRpwIDAQABo4IBgjCCAX4wEwYDVR0lBAwwCgYIKwYB # BQUHAwMwDgYDVR0PAQH/BAQDAgbAMBsGCSsGAQQBgjcVCgQOMAwwCgYIKwYBBQUH # AwMwHQYDVR0OBBYEFOKbNkkiAht2GxCISJMJxLg4gOC9MB0GA1UdEQQWMBSBEm9u # ZHJlakBzZXZlY2VrLmNvbTAfBgNVHSMEGDAWgBQNnMgyfdUi8l9UfithS4FQ88Vs # wDBSBgNVHR8ESzBJMEegRaBDhkFodHRwOi8vcGtpLnNldmVjZWsuY29tL0NBL1Nl # dmVjZWslMjBFbnRlcnByaXNlJTIwUm9vdCUyMENBKDEpLmNybDCBhgYIKwYBBQUH # AQEEejB4ME0GCCsGAQUFBzAChkFodHRwOi8vcGtpLnNldmVjZWsuY29tL0NBL1Nl # dmVjZWslMjBFbnRlcnByaXNlJTIwUm9vdCUyMENBKDEpLmNydDAnBggrBgEFBQcw # AYYbaHR0cDovL3BraS5zZXZlY2VrLmNvbS9vY3NwMA0GCSqGSIb3DQEBCwUAA4IC # AQCfr6XDtt/O8OBr+X5l49UBLaJrjUXHkAHofdC7p7BLCXIs4GYIti1lf6pas5yB # Q428aKITITq/vEHUTyiiyKtzVkafILWXXKPxy+zmmuw9odB3Hea4ECNpcaG8UNtz # vMm1Dr0ZrkENhcv6I3tNhRr2AOE9AKOfnVEullFD/mZqfmaNkhpnl31jk7OMSUQc # oY8qD6BDQP9371C10gJOmp57sHfPa4Vn5E4aNzn8+o9C9HI7nNagZF5BamKOFdR2 # ui7K3krMbTuDHo+ZcA9nHnzZqiVKpEBFu8lGv9Mf+GDb9yxz6EjV3xS2RcnywX2v # z0VUt2NGno8LudrnWpgrRy4Sl7x6FwVVKtS/o7zFSIiHgntIKFv8urSKSTukCLFK # Y9fBIDDlWFV1ZV1DNpNWxnexWIRv2AH7YlzKQCA4Rysn01hVeBGsWFkCr9J33LmV # enQYpk9eoYMPRwAYg48r65wOOOzLvmyLSGllH88BMvmTQ9myXqwp6NDH1psljXTl # PUbpf7w6IZwsY0dhGhP9iyqbcrGdK0Bnf8Za6Qdj3iXtwd1VgpatFZrxOM5KawCL # pkYl1ABupbzNpWzmC+nfymqwbYiCogPt1vHOyF4EJ73ExVDCqXkpiNvFRqmu1eaZ # IOdbPCdl00a9rk52NKqo/BUsw16TKsDEYTA/7ACbEsnERzCCBmowggVSoAMCAQIC # EAMBmgI6/1ixa9bV6uYX8GYwDQYJKoZIhvcNAQEFBQAwYjELMAkGA1UEBhMCVVMx # FTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNv # bTEhMB8GA1UEAxMYRGlnaUNlcnQgQXNzdXJlZCBJRCBDQS0xMB4XDTE0MTAyMjAw # MDAwMFoXDTI0MTAyMjAwMDAwMFowRzELMAkGA1UEBhMCVVMxETAPBgNVBAoTCERp # Z2lDZXJ0MSUwIwYDVQQDExxEaWdpQ2VydCBUaW1lc3RhbXAgUmVzcG9uZGVyMIIB # IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo2Rd/Hyz4II14OD2xirmSXU7 # zG7gU6mfH2RZ5nxrf2uMnVX4kuOe1VpjWwJJUNmDzm9m7t3LhelfpfnUh3SIRDsZ # yeX1kZ/GFDmsJOqoSyyRicxeKPRktlC39RKzc5YKZ6O+YZ+u8/0SeHUOplsU/UUj # joZEVX0YhgWMVYd5SEb3yg6Np95OX+Koti1ZAmGIYXIYaLm4fO7m5zQvMXeBMB+7 # NgGN7yfj95rwTDFkjePr+hmHqH7P7IwMNlt6wXq4eMfJBi5GEMiN6ARg27xzdPpO # 2P6qQPGyznBGg+naQKFZOtkVCVeZVjCT88lhzNAIzGvsYkKRrALA76TwiRGPdwID # AQABo4IDNTCCAzEwDgYDVR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwFgYDVR0l # AQH/BAwwCgYIKwYBBQUHAwgwggG/BgNVHSAEggG2MIIBsjCCAaEGCWCGSAGG/WwH # ATCCAZIwKAYIKwYBBQUHAgEWHGh0dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMw # ggFkBggrBgEFBQcCAjCCAVYeggFSAEEAbgB5ACAAdQBzAGUAIABvAGYAIAB0AGgA # aQBzACAAQwBlAHIAdABpAGYAaQBjAGEAdABlACAAYwBvAG4AcwB0AGkAdAB1AHQA # ZQBzACAAYQBjAGMAZQBwAHQAYQBuAGMAZQAgAG8AZgAgAHQAaABlACAARABpAGcA # aQBDAGUAcgB0ACAAQwBQAC8AQwBQAFMAIABhAG4AZAAgAHQAaABlACAAUgBlAGwA # eQBpAG4AZwAgAFAAYQByAHQAeQAgAEEAZwByAGUAZQBtAGUAbgB0ACAAdwBoAGkA # YwBoACAAbABpAG0AaQB0ACAAbABpAGEAYgBpAGwAaQB0AHkAIABhAG4AZAAgAGEA # cgBlACAAaQBuAGMAbwByAHAAbwByAGEAdABlAGQAIABoAGUAcgBlAGkAbgAgAGIA # eQAgAHIAZQBmAGUAcgBlAG4AYwBlAC4wCwYJYIZIAYb9bAMVMB8GA1UdIwQYMBaA # FBUAEisTmLKZB+0e36K+Vw0rZwLNMB0GA1UdDgQWBBRhWk0ktkkynUoqeRqDS/Qe # icHKfTB9BgNVHR8EdjB0MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20v # RGlnaUNlcnRBc3N1cmVkSURDQS0xLmNybDA4oDagNIYyaHR0cDovL2NybDQuZGln # aWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEQ0EtMS5jcmwwdwYIKwYBBQUHAQEE # azBpMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQQYIKwYB # BQUHMAKGNWh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3Vy # ZWRJRENBLTEuY3J0MA0GCSqGSIb3DQEBBQUAA4IBAQCdJX4bM02yJoFcm4bOIyAP # gIfliP//sdRqLDHtOhcZcRfNqRu8WhY5AJ3jbITkWkD73gYBjDf6m7GdJH7+IKRX # rVu3mrBgJuppVyFdNC8fcbCDlBkFazWQEKB7l8f2P+fiEUGmvWLZ8Cc9OB0obzpS # CfDscGLTYkuw4HOmksDTjjHYL+NtFxMG7uQDthSr849Dp3GdId0UyhVdkkHa+Q+B # 0Zl0DSbEDn8btfWg8cZ3BigV6diT5VUW8LsKqxzbXEgnZsijiwoc5ZXarsQuWaBh # 3drzbaJh6YoLbewSGL33VVRAA5Ira8JRwgpIr7DUbuD0FAo6G+OPPcqvao173NhE # MIIGzTCCBbWgAwIBAgIQBv35A5YDreoACus/J7u6GzANBgkqhkiG9w0BAQUFADBl # MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 # d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv # b3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMjExMTEwMDAwMDAwWjBiMQswCQYDVQQG # EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl # cnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBBc3N1cmVkIElEIENBLTEwggEiMA0G # CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDogi2Z+crCQpWlgHNAcNKeVlRcqcTS # QQaPyTP8TUWRXIGf7Syc+BZZ3561JBXCmLm0d0ncicQK2q/LXmvtrbBxMevPOkAM # Rk2T7It6NggDqww0/hhJgv7HxzFIgHweog+SDlDJxofrNj/YMMP/pvf7os1vcyP+ # rFYFkPAyIRaJxnCI+QWXfaPHQ90C6Ds97bFBo+0/vtuVSMTuHrPyvAwrmdDGXRJC # geGDboJzPyZLFJCuWWYKxI2+0s4Grq2Eb0iEm09AufFM8q+Y+/bOQF1c9qjxL6/s # iSLyaxhlscFzrdfx2M8eCnRcQrhofrfVdwonVnwPYqQ/MhRglf0HBKIJAgMBAAGj # ggN6MIIDdjAOBgNVHQ8BAf8EBAMCAYYwOwYDVR0lBDQwMgYIKwYBBQUHAwEGCCsG # AQUFBwMCBggrBgEFBQcDAwYIKwYBBQUHAwQGCCsGAQUFBwMIMIIB0gYDVR0gBIIB # yTCCAcUwggG0BgpghkgBhv1sAAEEMIIBpDA6BggrBgEFBQcCARYuaHR0cDovL3d3 # dy5kaWdpY2VydC5jb20vc3NsLWNwcy1yZXBvc2l0b3J5Lmh0bTCCAWQGCCsGAQUF # BwICMIIBVh6CAVIAQQBuAHkAIAB1AHMAZQAgAG8AZgAgAHQAaABpAHMAIABDAGUA # cgB0AGkAZgBpAGMAYQB0AGUAIABjAG8AbgBzAHQAaQB0AHUAdABlAHMAIABhAGMA # YwBlAHAAdABhAG4AYwBlACAAbwBmACAAdABoAGUAIABEAGkAZwBpAEMAZQByAHQA # IABDAFAALwBDAFAAUwAgAGEAbgBkACAAdABoAGUAIABSAGUAbAB5AGkAbgBnACAA # UABhAHIAdAB5ACAAQQBnAHIAZQBlAG0AZQBuAHQAIAB3AGgAaQBjAGgAIABsAGkA # bQBpAHQAIABsAGkAYQBiAGkAbABpAHQAeQAgAGEAbgBkACAAYQByAGUAIABpAG4A # YwBvAHIAcABvAHIAYQB0AGUAZAAgAGgAZQByAGUAaQBuACAAYgB5ACAAcgBlAGYA # ZQByAGUAbgBjAGUALjALBglghkgBhv1sAxUwEgYDVR0TAQH/BAgwBgEB/wIBADB5 # BggrBgEFBQcBAQRtMGswJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0 # LmNvbTBDBggrBgEFBQcwAoY3aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0Rp # Z2lDZXJ0QXNzdXJlZElEUm9vdENBLmNydDCBgQYDVR0fBHoweDA6oDigNoY0aHR0 # cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNy # bDA6oDigNoY0aHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJl # ZElEUm9vdENBLmNybDAdBgNVHQ4EFgQUFQASKxOYspkH7R7for5XDStnAs0wHwYD # VR0jBBgwFoAUReuir/SSy4IxLVGLp6chnfNtyA8wDQYJKoZIhvcNAQEFBQADggEB # AEZQPsm3KCSnOB22WymvUs9S6TFHq1Zce9UNC0Gz7+x1H3Q48rJcYaKclcNQ5IK5 # I9G6OoZyrTh4rHVdFxc0ckeFlFbR67s2hHfMJKXzBBlVqefj56tizfuLLZDCwNK1 # lL1eT7EF0g49GqkUW6aGMWKoqDPkmzmnxPXOHXh2lCVz5Cqrz5x2S+1fwksW5Etw # TACJHvzFebxMElf+X+EevAJdqP77BzhPDcZdkbkPZ0XN1oPt55INjbFpjE/7WeAj # D9KqrgB87pxCDs+R1ye3Fu4Pw718CqDuLAhVhSK46xgaTfwqIa1JMYNHlXdx3LEb # S0scEJx3FMGdTy9alQgpECYxggQ/MIIEOwIBATB6MGwxCzAJBgNVBAYTAkNaMRcw # FQYDVQQIEw5DemVjaCBSZXB1YmxpYzENMAsGA1UEBxMEQnJubzEQMA4GA1UEChMH # U2V2ZWNlazEjMCEGA1UEAxMaU2V2ZWNlayBFbnRlcnByaXNlIFJvb3QgQ0ECCioc # hHAAAQAAAH8wDQYJYIZIAWUDBAIBBQCggYQwGAYKKwYBBAGCNwIBDDEKMAigAoAA # oQKAADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4w # DAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgwi1WAjz2tDNSWFyPFUCW4Z/K # 9H9G51yOrsyYm3/i8sowDQYJKoZIhvcNAQEBBQAEggEAGAQyvSCFb/QdJbzCt5Mi # wPhs3zcxpau8MOsc3t/70PLjS1+09ansS42LkCrRR2gjE20pcHNJekFb4M9vdCWL # k2hwz3bM1vkh1jVqUdK29Z/QJOdj5Giun9RUHXi6zmNiX3OKYCs6UzbHvXfHjyV2 # IBydKoPow6WO4vfwFODwGlVCJC8k+xN5KdnZ/HboVtuAL4ItTy5/orSdHJbv6AUq # 0oUIufVNac01C7hmE1K2bQgh9nn7kCzJ+lP3XsfSBDdRos3FOEVyeDu0Pq+YIDtw # u2Vc2WLs6eF/JK8e+DrpCgtpU/3RMUqqpquKiH141Ol6yk+7M1dyrIbQ0NGwcf+X # aqGCAg8wggILBgkqhkiG9w0BCQYxggH8MIIB+AIBATB2MGIxCzAJBgNVBAYTAlVT # MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j # b20xITAfBgNVBAMTGERpZ2lDZXJ0IEFzc3VyZWQgSUQgQ0EtMQIQAwGaAjr/WLFr # 1tXq5hfwZjAJBgUrDgMCGgUAoF0wGAYJKoZIhvcNAQkDMQsGCSqGSIb3DQEHATAc # BgkqhkiG9w0BCQUxDxcNMTkwOTIwMTgxOTIyWjAjBgkqhkiG9w0BCQQxFgQUuC9q # +QHAbWJR5zaqpRtq92Bc3gMwDQYJKoZIhvcNAQEBBQAEggEAeRaOdaV+Fs5TjxA9 # lkxWbdL6BrirtOnBdoi6krAkhLJU6Y1KXbdSv20XboI+78kP4VslPsdNgdIymlE/ # 3P0y7K6+jVzih7BR1Hgl78TXHjKLu71BYAbOZP5frUt9g+jK4AQon+wH8U9MEhYF # BEZe4BmHgJh5FmEoIcEz1oqivpPCbSUupr6Q0yTzI0mO6H1qdVgAvi49Ag3LDTBX # 05OCpt23meBea0TYuGnnaXWWmqzPR7F5sZhl5HS5LIiBmVwlIZWqzLTo/alBfav2 # rwG99o4k36qnhT5J9zu8FOhNjr2H36ttruBLcEIl1lIoWHUJcev7Jg2b1YefX9LI # vx0V7Q== # SIG # End signature block