DTX.Cloud.Management.psm1

class DTXPSException : System.Exception {
  [string]$DTXStackInfo
  DTXPSException([string]$Message):base($Message) {
    $this.DTXStackInfo = ""
  } 
  DTXPSException([string]$Message, [string]$scriptStackTrace):base($Message) {
    $this.DTXStackInfo = $scriptStackTrace
  } 
  DTXPSException([string]$Message, [string]$scriptStackTrace, [Exception]$InnerException):base($Message, $InnerException) {
    $this.DTXStackInfo = $scriptStackTrace
  } 
}
class DTXLogger {
    hidden static [void] Log([string]$LogLevel, [string]$Message) {
        $_logLevel = $LogLevel.ToUpper()
        $_message = "$( Get-Date -Format "yyyy:MM:dd-hh:mm:ss" ) [$_logLevel] $Message"
        $colors = @{
            WARNING  = "Yellow"
            ERROR    = "Red"
            CRITICAL = "Red"
            DEBUG    = "Cyan"
        }
        if ($Env:DTX_DISABLE_COLORS -or $_logLevel -eq "INFO") {
            Write-Host $_message
        }
        else {
            Write-Host -ForegroundColor $colors[$_logLevel] $_message
        }
    }
    static [void] LogInfo([string]$Message) {
        [DTXLogger]::Log("Info", $Message)
    }
    static [void] LogError([string]$Message) {
        [DTXLogger]::Log("Error", $Message)
    }
    static [void] LogWarning([string]$Message) {
        [DTXLogger]::Log("Warning", $Message)
    }
    static [void] LogCritical([string]$Message, [object]$Exception, [bool]$ThrowException) {
        $_message = $Message
        if ([string]::IsNullOrEmpty($Message) -and ($Exception -is [System.Exception])) {
            $_message = $Exception.Message
        }
        elseif ([string]::IsNullOrEmpty($Message) -and ($Exception -is [System.Management.Automation.ErrorRecord])) {
            $_message = $Exception.Exception.Message
        }
        elseif ([string]::IsNullOrEmpty($Message)) {
            $_message = "An unknown error occurred."
        }
        [DTXLogger]::Log("Critical", $_message)
        if ($ThrowException) {
            switch ($Exception) {
                { $_ -is [System.Management.Automation.ErrorRecord] } {
                    throw [DTXPSException]::new($_message, $_.ScriptStackTrace, $_.Exception)
                }
                { $_ -is [System.Exception] } {
                    throw [DTXPSException]::new($_message, "", $_)
                }
                { $_ -eq $null } {
                    throw [DTXPSException]::new($_message, "", $null)
                }
                default {
                    throw "[DTXLogger]::LogCritical You have passed an invalid exception type. Please provide an ErrorRecord or an Exception object."
                }
            }
        }
    }
    static [void] LogCritical([string]$Message) {
        [DTXLogger]::LogCritical($Message, $null, $false)
    }
    static [void] LogCritical([object]$Exception) {
        [DTXLogger]::LogCritical($null, $Exception, $false)
    }
    static [void] LogCritical([string]$Message, [object]$Exception) {
        [DTXLogger]::LogCritical($Message, $null, $false)
    }
    static [void] LogCriticalAndThrow([string]$Message) {
        [DTXLogger]::LogCritical($Message, $null, $true)
    }
    static [void] LogCriticalAndThrow([object]$Exception) {
        [DTXLogger]::LogCritical($null, $Exception, $true)
    }
    static [void] LogCriticalAndThrow([string]$Message, [object]$Exception) {
        [DTXLogger]::LogCritical($Message, $Exception, $true)
    }
    static [void] LogDebug([string]$Message) {
        if (($Env:DTX_DEBUG_MODE -eq "true") -or ($Env:DTX_DEBUG_MODE -eq $true)) {
            [DTXLogger]::Log("Debug", $Message)
        }
    }
    static [void] LogSeparator() {
        [DTXLogger]::Log("Info", "---------------------------------------------------------------------")
    }
}
class DTXUtils {
    static [object] UsingObject(
        [object] $InputObject,
        [scriptblock] $ScriptBlock
    ) {
        try {
            if ($null -ne $InputObject) {
                [DTXLogger]::LogDebug("[DTXUtils::UsingObject] Executing script block with input object: $($InputObject.GetType().Name)") 
                return (& $ScriptBlock $InputObject)
            }
            else {
                [DTXLogger]::LogDebug("[DTXUtils::UsingObject] Executing script block without input object...")
                return (& $ScriptBlock)
            }
        }
        finally {
            if ($null -ne $InputObject -and $InputObject -is [System.IDisposable]) {
                [DTXLogger]::LogDebug("[DTXUtils::UsingObject] Disposing object: $($InputObject.GetType().Name)")
                $InputObject.Dispose()
                [DTXLogger]::LogDebug("[DTXUtils::UsingObject] Object disposed.")
            }
        }
    }
    static [void] WriteStringToFileWithRetry(
        [string]$StringContent,
        [string]$FilePath,
        [int]$RetryCount,
        [int]$WaitSeconds
    ) {
        [DTXLogger]::LogDebug("[DTXUtils::WriteStringToFileWithRetry] Writing string to file '$FilePath' with retry parameters: RetryCount=$RetryCount, WaitSeconds=$WaitSeconds")
        $retryAttempts = 0
        do {
            try {
                $StringContent | Out-File -Force -FilePath $FilePath
                break
            }
            catch {
                if ($retryAttempts -ge $RetryCount) {
                    [DTXLogger]::LogCriticalAndThrow("[DTXUtils::WriteStringToFileWithRetry] Failed to write to file '$FilePath' after $($RetryCount + 1) attempts.")
                }
                [DTXLogger]::LogWarning("[DTXUtils::WriteStringToFileWithRetry] Failed to write to file '$FilePath'. Retrying in $WaitSeconds seconds... Attempt: $($retryAttempts + 1)")
                Start-Sleep -Seconds $WaitSeconds
                $retryAttempts++
            }
        } while ($retryAttempts -le $RetryCount)
    }
    static [void] WriteStringToFileWithRetry([string]$StringContent, [string]$FilePath) {
        [DTXUtils]::WriteStringToFileWithRetry($StringContent, $FilePath, 3, 2)
    }
    static [string] GetSSLCertificateFingerprint(
        [string]$Hostname,
        [int]$Port,
        [string]$HashAlgorithm
    ) {
        if ($HashAlgorithm -notin @('SHA1', 'SHA256', 'SHA384', 'SHA512')) {
            [DTXLogger]::LogCriticalAndThrow("[DTXUtils::GetSSLCertificateFingerprint] Invalid HashAlgorithm: $HashAlgorithm. Supported values are 'SHA1', 'SHA256', 'SHA384', 'SHA512'.")
        }
        if ($Port -lt 1 -or $Port -gt 65535) {
            [DTXLogger]::LogCriticalAndThrow("[DTXUtils::GetSSLCertificateFingerprint] Invalid Port: $Port. Port number must be between 1 and 65535.")
        }
        Add-Type -AssemblyName System.Security
        $tcpClient = New-Object System.Net.Sockets.TcpClient
        $tcpClient.Connect($hostname, $port)
        $sslStream = New-Object System.Net.Security.SslStream($tcpClient.GetStream(), $false, {
                param($s, $certificate, $chain, $sslPolicyErrors)
                return $true 
            }
        )
        $fingerprint = $null
        try {
            $sslStream.AuthenticateAsClient($hostname)
            $remoteCertificate = $sslStream.RemoteCertificate
            if ($HashAlgorithm -eq 'SHA1') {
                $sha1 = New-Object System.Security.Cryptography.SHA1Managed
                $fingerprint = ($sha1.ComputeHash($remoteCertificate.GetRawCertData()) | ForEach-Object { $_.ToString("x2") }) -join ''
            }
            if ($HashAlgorithm -eq 'SHA256') {
                $sha256 = New-Object System.Security.Cryptography.SHA256Managed
                $fingerprint = ($sha256.ComputeHash($remoteCertificate.GetRawCertData()) | ForEach-Object { $_.ToString("x2") }) -join ''
            }
            if ($HashAlgorithm -eq 'SHA384') {
                $sha384 = New-Object System.Security.Cryptography.SHA384Managed
                $fingerprint = ($sha384.ComputeHash($remoteCertificate.GetRawCertData()) | ForEach-Object { $_.ToString("x2") }) -join ''
            }
            if ($HashAlgorithm -eq 'SHA512') {
                $sha512 = New-Object System.Security.Cryptography.SHA512Managed
                $fingerprint = ($sha512.ComputeHash($remoteCertificate.GetRawCertData()) | ForEach-Object { $_.ToString("x2") }) -join ''
            }
        }
        catch {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] Failed to get SSL Cert Fingerprint" -ThrowException -Exception $_
        }
        finally {
            if ($sslStream) {
                $sslStream.Dispose()
            }
            if ($tcpClient) {
                $tcpClient.Dispose()
            }
        }
        if ($null -ne $fingerprint) {
            $fingerprint = $fingerprint.ToLower()
        }
        return $fingerprint
    }
    static [string] GetSSLCertificateFingerprint() {
        return [DTXUtils]::GetSSLCertificateFingerprint('localhost', 443, 'SHA256')
    }
}
class DTXKeyValueStore {
    hidden [string]$Path
    DTXKeyValueStore(
        [string]$Path
    ) {
        $this.Path = $Path
        $this.InitStore()
    }
    hidden [void] InitStore() {
        if (-not (Test-Path -Path $this.Path)) {
            New-Item -Path $this.Path -ItemType File -Force
            Set-Content -Path $this.Path -Value "{}" -Force
        }
    }
    hidden [hashtable] ReadStoreContents() {
        $store = Get-Content -Path $this.Path -Raw | ConvertFrom-Json -AsHashtable -Depth 100
        if ($null -eq $store) {
            $store = New-Object System.Collections.Hashtable
        }
        return $store
    }
    [hashtable] GetStoreContent() {
        return $this.ReadStoreContents()
    }
    [void] ResetStore() {
        Remove-Item -Path $this.Path -Force
        $this.InitStore()
    }
    [void] SetValue(
        [string]$Key,
        [object]$Value
    ) {
        $store = $this.ReadStoreContents()
        $store[$Key] = $Value
        $storeJson = $store | ConvertTo-Json -Depth 100 -EnumsAsStrings
        Set-Content -Path $this.Path -Value $storeJson -Force
    }
    [object] GetValue(
        [string]$Key
    ) {
        $store = $this.ReadStoreContents()
        if (-not $store.ContainsKey($Key)) {
            return $null
        }
        return $store[$Key]
    }
    [void] RemoveKey(
        [string]$Key
    ) {
        $store = $this.ReadStoreContents()
        if ($store.ContainsKey($Key)) {
            $store.Remove($Key)
            $storeJson = $store | ConvertTo-Json -Depth 100 -EnumsAsStrings
            Set-Content -Path $this.Path -Value $storeJson -Force
        }
    }
    [bool] HasKey(
        [string]$Key
    ) {
        $store = $this.ReadStoreContents()
        return $store.ContainsKey($Key)
    }
}
class ScheduledTasks {
  ScheduledTasks(
  ) { }
  [void] WriteLog  (
    [string]$LogLevel,
    [string]$Message
  ) {
    $_logLevel = $LogLevel.ToUpper()
    $_message = "$( Get-Date -Format "yyyy:MM:dd-hh:mm:ss" ) [$_logLevel] $Message"
    $colors = @{
      WARNING  = "Yellow"
      ERROR    = "Red"
      CRITICAL = "Red"
    }
    if ($Env:DTX_DISABLE_COLORS -or $_logLevel -eq "INFO") {
      Write-Host $_message
    }
    else {
      Write-Host -ForegroundColor $colors[$_logLevel] $_message
    }
  }
  [void] RemoveScheduledTask(
    [string] $Name
  ) {
    $the_task = $this.GetScheduledTask($Name, $false)
    if ($the_task -eq $null) {
      $this.WriteLog("INFO", "Task doesnt exist, returning success for task name: $Name")
    }
    else {
      $this.WriteLog("INFO", "Task does exist, performing delete for task name: $Name")
      $Service = new-object -ComObject("Schedule.Service")
      $Service.Connect()
      $TaskFolder = $Service.GetFolder("\")
      $TaskFolder.DeleteTask($Name, $null)
      $this.WriteLog("INFO", "Task deleted")
    }
  }
  [object] GetScheduledTask(
    [string]$Name,
    [bool] $Throw
  ) {
    $mytasks = $this.ListNonSystemScheduledTasks()
    foreach ($task in $mytasks) {
      if ($task.Name -eq $Name) {
        return $task
      }
    }
    if ($Throw) {
      throw (New-Object DTXPSException("[$( $MyInvocation.MyCommand )] Unable to find Task with name : $Name"))
    }
    return $null
  }
  [array] ListNonSystemScheduledTasks(
  ) {
    $sch = New-Object -ComObject Schedule.Service
    $sch.Connect("localhost")
    $tasks = $sch.GetFolder("\").GetTasks(0)
    $ret_val = @()
    foreach ($task in $tasks) {
      $Author = ([regex]::split($task.xml, '<Author>|</Author>'))[1]
      $UserId = ([regex]::split($task.xml, '<UserId>|</UserId>'))[1]
      $Description = ([regex]::split($task.xml, '<Description>|</Description>'))[1]
      $Action = ([regex]::split($task.xml, '<Command>|</Command>'))[1]
      $Arguments = ([regex]::split($task.xml, '<Arguments>|</Arguments>'))[1]
      $RunLevel = ([regex]::split($task.xml, '<RunLevel>|</RunLevel>'))[1]
      $LogonType = ([regex]::split($task.xml, '<LogonType>|</LogonType>'))[1]
      $DateRegistered = ([regex]::split($task.xml, '<Date>|</Date>'))[1]
      Switch ($task.State) {
        0 { $Status = "Unknown" }
        1 { $Status = "Disabled" }
        2 { $Status = "Queued" }
        3 { $Status = "Ready" }
        4 { $Status = "Running" }
      }
      $myoutput = $task | select @{ label = "ComputerName"; expression = { $computer } },
      Name,
      Path,
      Enabled,
      @{ label = "Action"; expression = { $Action } },
      @{ label = "Arguments"; expression = { $Arguments } },
      @{ label = "UserId"; expression = { $UserId } },
      LastRunTime,
      NextRunTime,
      @{ label = "Status"; expression = { $Status } },
      @{ label = "Author"; expression = { $Author } },
      @{ label = "RunLevel"; expression = { $RunLevel } },
      @{ label = "Description"; expression = { $Description } },
      @{ label = "DateCreated"; expression = { $DateRegistered } },
      NumberOfMissedRuns,
      LastTaskResult
      $ret_val += $myoutput
    } 
    return $ret_val
  }
}
class DTXXmlUtils {
    static [bool] CompareXmlAttribute([System.Xml.XmlElement] $Element, [string] $Attribute, [string] $Value) {
        if ($Element -and $Element.HasAttribute($Attribute)) {
            return $Element.GetAttribute($Attribute) -eq $Value
        }
        return $false
    }
    static [void] UpdateXmlAttribute([System.Xml.XmlElement] $Element, [string] $Attribute, [string] $Value) {
        if ($Element -and $Element.HasAttribute($Attribute)) {
            $Element.SetAttribute($Attribute, $Value)
        }
    }
    static [string] FormatXml([xml]$XmlContent, [bool]$EnableIndentation, [int]$IndentChars, [bool]$NewLineOnAttributes) {
        if ($null -eq $XmlContent) {
            throw [System.ArgumentNullException]::new("XmlContent cannot be null.")
        }
        $result = [DTXUtils]::UsingObject(
            ($memoryStream = New-Object System.IO.MemoryStream),
            {
                $settings = New-Object System.Xml.XmlWriterSettings
                $settings.Indent = $EnableIndentation
                $settings.IndentChars = " " * $IndentChars
                $settings.NewLineOnAttributes = $NewLineOnAttributes
                [DTXUtils]::UsingObject(($writer = [System.Xml.XmlWriter]::Create($memoryStream, $settings)), {
                        $XmlContent.Save($writer)
                        $writer.Flush()
                    }) | Out-Null
                $memoryStream.Position = 0
                return [DTXUtils]::UsingObject(($formattedXml = New-Object System.IO.StreamReader($memoryStream)), {
                        return $formattedXml.ReadToEnd()
                    })
            }
        )
        $result = $result -replace '^\s*(<\?xml)', '$1'
        return $result
    }
    static [string] FormatXml([xml]$XmlContent) {
        return [DTXXmlUtils]::FormatXml($XmlContent, $true, 4, $true)
    }
    static [bool] TestXmlContentIsValid([string] $XmlContent) {
        try {
            $xmlDocument = New-Object System.Xml.XmlDocument
            $xmlDocument.LoadXml($XmlContent)
            $xmlDocument = $null
            return $true
        }
        catch {
            return $false
        }
    }
}
class DTXJsonUtils {
    static [string] RemoveJsonComments(
        [string]$JsonContent
    ) {
        $inString = $false
        $escaped = $false
        $inSingleLineComment = $false
        $inMultiLineComment = $false
        $result = ""
        for ($i = 0; $i -lt $JsonContent.Length; $i++) {
            $char = $JsonContent[$i]
            if ($inSingleLineComment) {
                if ($char -eq "`n" -or $char -eq "`r") {
                    $inSingleLineComment = $false
                    $result += $char
                }
                continue
            }
            if ($inMultiLineComment) {
                if ($char -eq "*" -and ($i + 1) -lt $JsonContent.Length -and $JsonContent[$i + 1] -eq "/") {
                    $inMultiLineComment = $false
                    $i++
                }
                continue
            }
            if ($inString) {
                if ($escaped) {
                    $escaped = $false
                }
                elseif ($char -eq "\") {
                    $escaped = $true
                }
                elseif ($char -eq '"') {
                    $inString = $false
                }
            }
            else {
                if ($char -eq "/" -and ($i + 1) -lt $JsonContent.Length) {
                    $nextChar = $JsonContent[$i + 1]
                    if ($nextChar -eq "/") {
                        $inSingleLineComment = $true
                        $i++
                        continue
                    }
                    elseif ($nextChar -eq "*") {
                        $inMultiLineComment = $true
                        $i++
                        continue
                    }
                }
                elseif ($char -eq '"') {
                    $inString = $true
                }
            }
            $result += $char
        }
        return $result
    }
}
class DTXTestUtils {
  static [bool] HTTPService($Uri, $ExpectedStatusCode, $ExpectedStatusCodeRange, $WaitTimeInSeconds, $MaxRetryCount, $SkipCertificateCheck) {
    if (!$ExpectedStatusCode -and !$ExpectedStatusCodeRange) {
      [DTXLogger]::LogCriticalAndThrow("[DTXTestUtils] Either 'ExpectedStatusCode' or 'ExpectedStatusCodeRange' params have to be provided.")
    }
    if ($ExpectedStatusCode -and $ExpectedStatusCodeRange) {
      [DTXLogger]::LogCriticalAndThrow("[DTXTestUtils] Both 'ExpectedStatusCode' and 'ExpectedStatusCodeRange' are set. You need to provide only one of them.")
    }
    $retryCount = 0
    do {
      $retryCount++
      try {
        $params = @{
          Uri                  = $Uri
          SkipCertificateCheck = $SkipCertificateCheck
          TimeoutSec           = 1
        }
        $req = Invoke-WebRequest @params 
        $statusCode = $req.StatusCode
        if ($ExpectedStatusCode -and ($statusCode -eq $ExpectedStatusCode)) {
          [DTXLogger]::LogInfo("[DTXTestUtils] The service at $Uri is healthy.")
          return $true
        }
        if ($ExpectedStatusCodeRange) {
          $startRange = ($ExpectedStatusCodeRange -split "-")[0]
          $endRange = ($ExpectedStatusCodeRange -split "-")[1]
          if (($statusCode -ge $startRange) -and ($statusCode -le $endRange)) {
            [DTXLogger]::LogInfo("[DTXTestUtils] The service at $Uri is healthy.")
            return $true
          }
        }
      }
      catch [System.Net.Sockets.SocketException] {
        [DTXLogger]::LogWarning("[DTXTestUtils] The service at $Uri is not ready to accept connections.")
        [DTXLogger]::LogWarning("[DTXTestUtils] Retrying in $WaitTimeInSeconds seconds... (Attempt $retryCount of $MaxRetryCount)")
        Start-Sleep -Seconds $WaitTimeInSeconds
        continue
      }
      catch [System.Threading.Tasks.TaskCanceledException] {
        if ($_.Exception.Message -like "*request*canceled*HttpClient.Timeout*elapsing*") {
          [DTXLogger]::LogWarning("[DTXTestUtils] The service at $Uri is not ready to accept connections.")
          [DTXLogger]::LogWarning("[DTXTestUtils] Retrying in $WaitTimeInSeconds seconds... (Attempt $retryCount of $MaxRetryCount)")
          Start-Sleep -Seconds $WaitTimeInSeconds
          continue
        }
        throw $_
      }
      catch [System.Security.Authentication.AuthenticationException] {
        throw $_
      }
      catch {
        [DTXLogger]::LogWarning("[DTXTestUtils] Error occurred: $_")
        [DTXLogger]::LogWarning("[DTXTestUtils] Retrying in $WaitTimeInSeconds seconds... (Attempt $retryCount of $MaxRetryCount)")
        Start-Sleep -Seconds $WaitTimeInSeconds
        continue
      }
    } while ($retryCount -le ($MaxRetryCount - 1))
    [DTXLogger]::LogInfo("[DTXTestUtils] The service at $Uri is NOT healthy.")
    return $false
  }
}
class DTXS3Utils {
    static [string] GetFile($BucketName, $BucketKey, $DirectoryPath) {
        return [DTXS3Utils]::GetFile($BucketName, $BucketKey, $DirectoryPath, $false)
    }
    static [string] GetFile($BucketName, $BucketKey, $DirectoryPath, $PreserveFileName) {
        $ProgressPreference = 'SilentlyContinue'
        $bucketRegion = $null
        $path = $null
        try {
            if (!$DirectoryPath) {
                $DirectoryPath = [System.IO.Path]::GetTempPath()
                if (!$DirectoryPath) {
                    throw (New-Object DTXPSException("[$( $MyInvocation.MyCommand )] Failed to generate a temporary file path"))
                }
            }
            if ($PreserveFileName) {
                $path = (Join-Path $DirectoryPath $([System.IO.Path]::GetFileName($BucketKey)))
            }
            else {
                $extension = [System.IO.Path]::GetExtension($BucketKey)
                if ($extension) {
                    $path = (Join-Path $DirectoryPath "$( Get-Random )$extension")
                }
                else {
                    $path = (Join-Path $DirectoryPath "$( Get-Random )")
                }
            }
            $bucketRegion = (Get-S3BucketLocation -BucketName $BucketName -ErrorAction Stop).Value
            [DTXLogger]::LogInfo("[DTXS3Utils] Downloading file '$BucketKey' from bucket '$BucketName' in region '$bucketRegion' to file path: $path")
            $params = @{
                BucketName = $BucketName
                Key        = $BucketKey
                File       = $path
            }
            if ($bucketRegion) {
                $params.Region = $bucketRegion
            }
            Read-S3Object @params -ErrorAction Stop | Out-Null
        }
        catch {
            [DTXLogger]::LogCriticalAndThrow("[DTXS3Utils] Failed to download file '$BucketKey' from bucket '$BucketName' in region '$bucketRegion'.", $_)
        }
        [DTXLogger]::LogInfo("[DTXS3Utils] File downloaded to $path")
        return $path
    }
}
class DTXModuleMetadataProvider {
    hidden [System.Management.Automation.InvocationInfo]$Invocation
    hidden [hashtable]$ModuleManifest
    DTXModuleMetadataProvider(
        [System.Management.Automation.InvocationInfo]$Invocation
    ) {
        $this.Invocation = $Invocation
        if (-not $this.Invocation.MyCommand.Scriptblock.Module) {
            $this.LoadModuleManifest()
        }
    }
    hidden [void] LoadModuleManifest() {
        if ($env:DTX_IS_RUNNING_TESTS) {
            $manifestFiles = Get-ChildItem -Path "$PSScriptRoot/.." -Filter '*.psd1'
        }
        else {
            $manifestFiles = Get-ChildItem -Path $PSScriptRoot -Filter '*.psd1'
        }
        foreach ($file in $manifestFiles) {
            try {
                $manifest = Import-PowerShellDataFile -Path $file.FullName
                if ($manifest.ContainsKey('RootModule') -and $manifest.ContainsKey('ModuleVersion') -and $manifest.ContainsKey('GUID')) {
                    $this.ModuleManifest = $manifest
                    break
                }
            }
            catch {
            }
        }
        if (-not $this.ModuleManifest) {
            throw "Module manifest (.psd1) file not found in the current directory."
        }
    }
    [string] GetPSModuleVersion() {
        if ($this.Invocation.MyCommand.Scriptblock.Module) {
            return $this.Invocation.MyCommand.Scriptblock.Module.Version.ToString()
        }
        return $this.ModuleManifest.ModuleVersion.ToString()
    }
    [string] GetPSModulePrereleaseVersion() {
        if ($this.Invocation.MyCommand.Scriptblock.Module) {
            return $this.Invocation.MyCommand.Scriptblock.Module.PrivateData.PSData.Prerelease
        }
        return $this.ModuleManifest.PrivateData.PSData.Prerelease
    }
    [string] GetPSModuleRealVersion() {
        $branchName = $this.GetBranchName()
        $myRealVersion = $this.GetPSModuleVersion()
        if (@("master", "main") -notcontains $branchName) {
            $myRealVersion += "-" + $this.GetPSModulePrereleaseVersion()
        }
        return $myRealVersion
    }
    [string] GetPSModuleName() {
        $theName = $null
        if ($this.Invocation.MyCommand.Scriptblock.Module) {
            $theName = $this.Invocation.MyCommand.Scriptblock.Module.Name
        }
        else {
            $theName = $this.ModuleManifest.RootModule
        }
        if (!$theName) {
            throw "Module name not found"
        }
        return $theName -replace '\.psm1$'
    }
    [string] GetBranchName() {
        if ($this.Invocation.MyCommand.Scriptblock.Module) {
            return $this.Invocation.MyCommand.Scriptblock.Module.PrivateData.BranchName
        }
        return $this.ModuleManifest.PrivateData.BranchName
    }
    [string] GetEnvironmentType() {
        $branchName = $this.GetBranchName()
        $envType = "production"
        if (@("master", "main") -notcontains $branchName) {
            $envType = "development"
        }
        return $envType
    }
}
class DTXPathProvider {
    hidden static [void] CreatePathIfNotExist([string]$thePath) {
        if (!(Test-Path $thePath)) {
            New-Item -Path $thePath -ItemType Directory -Force | Out-Null
        }
    }
    static [System.IO.DirectoryInfo] GetLocalDotmaticsPath() {
        $thePath = $null
        if ($env:DTX_LOCAL_DOTMATICS_PATH) {
            $thePath = $env:DTX_LOCAL_DOTMATICS_PATH
        }
        else {
            if ($global:IsWindows) {
                $thePath = Join-Path $env:PROGRAMDATA "dotmatics"
            }              
            elseif ($global:IsLinux -or $global:IsMacOS) {
                $thePath = Join-Path "/" "var" "local" "dotmatics"
            }
        }
        [DTXPathProvider]::CreatePathIfNotExist($thePath)
        return New-Object System.IO.DirectoryInfo($thePath)
    }
    static [System.IO.DirectoryInfo] GetLocalTempPath() {
        $thePath = [System.IO.Path]::Combine([DTXPathProvider]::GetLocalDotmaticsPath(), "temp")
        [DTXPathProvider]::CreatePathIfNotExist($thePath)
        return New-Object System.IO.DirectoryInfo($thePath)
    }
    static [System.IO.DirectoryInfo] GetLocalCachePath() {
        $thePath = [System.IO.Path]::Combine([DTXPathProvider]::GetLocalDotmaticsPath(), "cache")
        [DTXPathProvider]::CreatePathIfNotExist($thePath)
        return New-Object System.IO.DirectoryInfo($thePath)
    }
    static [System.IO.DirectoryInfo] GetLocalExternalFilesCachePath() {
        $thePath = [System.IO.Path]::Combine([DTXPathProvider]::GetLocalCachePath(), "external_files")
        [DTXPathProvider]::CreatePathIfNotExist($thePath)
        return New-Object System.IO.DirectoryInfo($thePath)
    }
    static [System.IO.DirectoryInfo] GetDevelopmentExternalFilesPath() {
        $rootPath = [System.IO.DirectoryInfo]::new($PSScriptRoot)
        $parentPath = $rootPath.Parent
        $devExternalFilesPath = [System.IO.Path]::Combine($parentPath.FullName, "ExternalFiles")
        return New-Object System.IO.DirectoryInfo([System.IO.Path]::GetFullPath($devExternalFilesPath))
    }
    static [System.IO.DirectoryInfo] GetLocalStateFilePath() {
        return New-Object System.IO.DirectoryInfo([System.IO.Path]::Combine([DTXPathProvider]::GetLocalDotmaticsPath(), "dtx_state.json"))
    }
    static [System.IO.DirectoryInfo] GetDiscoveryFilePath() {
        return New-Object System.IO.DirectoryInfo([System.IO.Path]::Combine([DTXPathProvider]::GetLocalDotmaticsPath(), "dtx_discovery.json"))
    }
    static [System.IO.DirectoryInfo] GetLocalDotmaticsSharedPath() {
        $thePath = $null
        if ($env:DTX_LOCAL_DOTMATICS_PATH) {
            $thePath = Join-Path $env:DTX_LOCAL_DOTMATICS_PATH "shared"
        }
        else {
            if ($global:IsWindows) {
                if (-not (Test-Path "D:\")) {
                    $thePath = Join-Path "C:" "dotmatics" "shared"
                }
                else {
                    $thePath = Join-Path "D:" "dotmatics" "shared"
                }
            }
            if ($global:IsLinux -or $global:IsMacOS) {
                $thePath = Join-Path "/" "var" "local" "dotmatics" "shared"
            }
        }
        [DTXPathProvider]::CreatePathIfNotExist($thePath)
        return $thePath
    }
    static [System.IO.DirectoryInfo] GetLocalDotmaticsSharedCertPath () {
        $thePath = Join-Path ([DTXPathProvider]::GetLocalDotmaticsSharedPath()) "tls"
        [DTXPathProvider]::CreatePathIfNotExist($thePath)
        return $thePath
    }
}
class DTXExternalFilesProvider {
    hidden [string]$className = $this.GetType().Name
    hidden [string]$S3BucketNameSSSMParamName
    hidden [string]$DefaultAwsRegion
    hidden [DTXKeyValueStore]$State
    hidden [DTXModuleMetadataProvider]$ModuleMeta
    hidden [System.IO.DirectoryInfo]$ExternalFilesCachePath
    hidden [System.IO.DirectoryInfo]$ExternalFilesCacheRootPath
    DTXExternalFilesProvider(
        [System.Management.Automation.InvocationInfo]$Invocation
    ) {
        $this.ModuleMeta = [DTXModuleMetadataProvider]::new($Invocation)
        $this.S3BucketNameSSSMParamName = "/cloud/public/regional/s3/v1/buckets/cf_extensions/name"
        $this.DefaultAwsRegion = "eu-west-1"
        $this.ExternalFilesCacheRootPath = [DTXPathProvider]::GetLocalExternalFilesCachePath()
        $this.ExternalFilesCachePath = [System.IO.Path]::Combine($this.ExternalFilesCacheRootPath , $this.ModuleMeta.GetPSModuleRealVersion())
        $this.State = [DTXKeyValueStore]::new([System.IO.Path]::Combine($this.ExternalFilesCachePath, "__state.json"))
    }
    hidden [string] GetExternalFilesS3Path() {
        return (@(
                "bin",
                $this.ModuleMeta.GetEnvironmentType(),
                "dot_po_cloudops_modules",
                $this.ModuleMeta.GetBranchName(),
                $this.ModuleMeta.GetPSModuleRealVersion()
            ) -join "/")
    }
    hidden [string] GetS3BucketName() {
        $params = @{
            Name           = $this.S3BucketNameSSSMParamName
            Region         = $this.GetDefaultAwsRegion()
            WithDecryption = $true
            ErrorAction    = "Stop"
        }
        return (Get-SSMParameterValue @params).Parameters[0].Value.ToString()
    }
    hidden [string] GetDefaultAwsRegion() {
        $theRegion = Get-DefaultAWSRegion
        if ($null -eq $theRegion) {
            $theRegion = $this.DefaultAwsRegion
        }
        else {
            $theRegion = $theRegion.Region
        }
        return $theRegion
    }
    hidden [string] GetS3BucketRegion() {
        $result = Get-S3BucketLocation -BucketName $this.GetS3BucketName()
        if ($result.Value) {
            return $result.Value
        }
        return "us-east-1"
    }
    hidden [void] ClearCache() {
        $installedModuleVersions = New-Object System.Collections.Generic.HashSet[string]
        Get-InstalledModule -Name $this.ModuleMeta.GetPSModuleName() -AllVersions | Select-Object -ExpandProperty Version | ForEach-Object { 
            $installedModuleVersions.Add($_.ToString()) 
        }
        $installedModuleVersions.Add($this.ModuleMeta.GetPSModuleRealVersion()) | Out-Null
        $cachePath = $this.ExternalFilesCacheRootPath
        $cachedModuleVersions = Get-ChildItem -Path $cachePath -Directory | Select-Object -ExpandProperty Name
        foreach ($cachedVersion in $cachedModuleVersions) {
            if ($installedModuleVersions -contains $cachedVersion) {
                continue
            }
            $cachedModuleVersionPath = Join-Path -Path $cachePath -ChildPath $cachedVersion
            try {
                Remove-Item -Path $cachedModuleVersionPath -Recurse -Force -ErrorAction Stop
                [DTXLogger]::LogInfo("[$($this.className)] Removed cached external files for module version $cachedVersion at $cachedModuleVersionPath")
            }
            catch {
                [DTXLogger]::LogError("[$($this.className)] Failed to remove cached external files for module version $cachedVersion at $cachedModuleVersionPath")
                throw $_
            }
        }
    }
    hidden [void] SyncFilesFromS3() {
        if ($this.State.GetValue("__status") -eq "synced") {
            [DTXLogger]::LogInfo("[$($this.className)] External files already synced. Nothing to do.")
            return
        }
        else {
            $this.State.SetValue("__status", "syncing")
            [DTXLogger]::LogInfo("[$($this.className)] Syncing external files...")
            try {
                $bucketName = $this.GetS3BucketName()
                $bucketRegion = $this.GetS3BucketRegion()
                $s3Objects = Get-S3Object -BucketName $bucketName -KeyPrefix $this.GetExternalFilesS3Path() -Region $bucketRegion
                foreach ($s3Object in $s3Objects) {
                    $s3ObjectFiltered = (($s3Object.Key -split $this.ModuleMeta.GetPSModuleRealVersion())[-1] -split "/")[-1]
                    $localFilePath = Join-Path -Path $this.ExternalFilesCachePath -ChildPath $s3ObjectFiltered
                    if ($this.State.HasKey($s3Object.Key) -and (Test-Path -Path $localFilePath)) {
                        continue
                    }
                    Read-S3Object -BucketName $bucketName -Key $s3Object.Key -File $localFilePath -Region $bucketRegion
                    $this.State.SetValue($s3Object.Key, @{ "Downloaded" = (Get-Date -Format "yyyy-MM-dd-hh-mm-ss") })
                }
            }
            catch {
                [DTXLogger]::LogError("[$($this.className)] Failed to sync external files.")
                $this.State.SetValue("__status", "failed")
                throw
            }
            $this.State.SetValue("__status", "synced")
            [DTXLogger]::LogInfo("[$($this.className)] External files synced.")
        }
    }
    hidden [void] Sync() {
        $this.SyncFilesFromS3()
        $this.ClearCache()
    }
    [System.IO.DirectoryInfo] GetFilePath([string]$FileName) {
        if ($FileName -eq "__state.json") {
            [DTXLogger]::LogWarning("[$($this.className)] File $FileName is a reserved name. Cannot be retrieved.")
            return $null
        }
        $devExternalFilesPath = [DTXPathProvider]::GetDevelopmentExternalFilesPath()
        if (Test-Path -Path $devExternalFilesPath) {
            $localPath = Join-Path $devExternalFilesPath $FileName
            if (Test-Path $localPath) {
                return New-Object System.IO.DirectoryInfo($localPath)
            }
        }
        $filePath = Join-Path -Path $this.externalFilesCachePath -ChildPath $FileName
        if (-not (Test-Path -Path $filePath)) {
            [DTXLogger]::LogInfo("[$($this.className)] File $FileName not found in cache. Trying to sync...")
            $this.Sync()
        }
        if (Test-Path -Path $filePath) {
            return New-Object System.IO.DirectoryInfo($filePath)
        }
        else {
            throw [System.IO.FileNotFoundException] "File $FileName not found in cache or S3."
        }
    }
}
class DTXStateProvider {
  hidden static [DTXKeyValueStore] GetKVStore() {
    return [DTXKeyValueStore]::new([DTXPathProvider]::GetLocalStateFilePath())
  }
  static [object]  GetValue([string]$Key) {
    return [DTXStateProvider]::GetKVStore().GetValue($Key)
  }
  static [object]  RemoveKey([string]$Key) {
    return [DTXStateProvider]::GetKVStore().RemoveKey($Key)
  }
  static [void]  SetValue([string]$Key, [object]$Value) {
    [DTXStateProvider]::GetKVStore().SetValue($Key, $Value)
  }
}
class DTXBrowserSysMonitor {
  [System.Collections.Hashtable] static Perform($monitoringIpsKey, $ApiEndpoint, $browserPropertiesDict) {
    $headers = [DTXBrowserSysMonitor]::GetApiCallHeaders($browserPropertiesDict, $monitoringIpsKey)
    return [DTXBrowserSysMonitor]::InvokeApi($ApiEndpoint, $headers)
  }
  hidden [hashtable] static GetApiCallHeaders($PropertiesData, $PropertyKey) {
    $headers = @{}
    if ($PropertiesData.Contains($PropertyKey)) {
      $ok_ip = $PropertiesData.$PropertyKey.split(",")[0] 
      $headers.Add("X-Forwarded-For", $ok_ip)
    }
    else {
    }
    return $headers
  }
  hidden [System.Collections.Hashtable] static InvokeApi($ApiEndpoint, $headers) {
    try {
      $response = Invoke-WebRequest -Uri $APIEndpoint  -TimeoutSec 15 -SkipCertificateCheck -Headers $headers
      if ($response -ne $null) {
        if (Test-Json -Json $response.Content -ErrorAction SilentlyContinue) {
          return $response.Content | ConvertFrom-Json -AsHashtable
        }
        else {
          [DTXLogger]::LogWarning("[DTXBrowserSysMonitor] Failed to detect data from Web Response as JSON, returning empty dict")
          [DTXLogger]::LogWarning("[DTXBrowserSysMonitor] Data: $($response.Content)")
          return New-Object System.Collections.Hashtable
        }
      }
      else {
        [DTXLogger]::LogWarning("[DTXBrowserSysMonitor] Failed to see response as non null, returning empty dict")
        return New-Object System.Collections.Hashtable
      }
    }
    catch {
      [DTXLogger]::LogWarning("[DTXBrowserSysMonitor] Failed to execute web request to SystemMonitor.do: `n $($_.Exception.Message | Out-String)")
      [DTXLogger]::LogWarning("[DTXBrowserSysMonitor] returning empty dict")
      return New-Object System.Collections.Hashtable
    }
  }
}
class DTXBrowser {
    hidden [string]$className = $this.GetType().Name
    hidden [String]$TomcatWebAppPath
    DTXBrowser(
        [string]$TomcatWebAppPath
    ) {
        $this.TomcatWebAppPath = $TomcatWebAppPath
    }
    [string] GetBrowserPlatformPath() {
        return Join-Path $this.TomcatWebAppPath  "browser"
    }
    [string] GetBrowserPlatformPropertiesFilePath() {
        return Join-Path $this.GetBrowserPlatformPath() "WEB-INF" "browser.properties"
    }
    [string] GetBrowserPlatformGifCachePath() {
        $thePath = Join-Path $this.GetBrowserPlatformPath() "gifcache" 
        if (!(Test-Path $thePath)) {
            [DTXLogger]::LogCriticalAndThrow("[$($this.className)] Unable to locate the Browser Platform Gifcache directory. The path $thePath does not exist")
        }
        return $thePath
    }
    [string] GetBrowserPlatformGifCacheToolkitPath() {
        $thePath = Join-Path $this.GetBrowserPlatformGifCachePath() "toolkit" 
        if (!(Test-Path $thePath)) {
            [DTXLogger]::LogCriticalAndThrow("[$($this.className)] Unable to locate the Browser Platform Gifcache directory. The path $thePath does not exist")
        }
        return $thePath
    }
    [Boolean] IsBrowserSystem() {
        if (Test-Path -Path $this.GetBrowserPlatformPropertiesFilePath()) {
            [DTXLogger]::LogInfo("[$($this.className)] Is a Browser System.")
            return $true
        }
        else {
            [DTXLogger]::LogInfo("[$($this.className)] Is NOT a Browser System.")
            return $false
        }
    }
    [hashtable]GetBrowserPropertiesData() {
        return ConvertFrom-StringData (Get-Content -Raw $this.GetBrowserPlatformPropertiesFilePath())
    }
}
class DTXTomcatUtils {
    static [hashtable] GetDesiredServerSettings(
        [hashtable]$DTXSystemInfo,
        [hashtable]$DTXDefaults,
        [int] $SettingsVersion
    ) {
        $_settings = $DTXDefaults.Tomcat.ServerSettings | Where-Object { $_.Version -eq $SettingsVersion }
        if ($_settings.Count -eq 0) {
            [DTXLogger]::LogCriticalAndThrow("[DTXTomcatUtils]::GetDesiredServerSettings - No server settings found for the current server settings version.")
        }
        $tomcatVersion = $DTXSystemInfo.AppInfo.Tomcat.Version
        $javaVersion = $DTXSystemInfo.AppInfo.Tomcat.Java.Version
        [DTXLogger]::LogInfo("[DTXTomcatUtils]::GetDesiredServerSettings - Detected Apache Tomcat version: $tomcatVersion")
        [DTXLogger]::LogInfo("[DTXTomcatUtils]::GetDesiredServerSettings - Detected Java version: $javaVersion")
        $unsupportedVersion = $false
        if ([System.Version] $_settings.JavaMinVersion -gt [System.Version] $javaVersion) {
            [DTXLogger]::LogWarning("[DTXTomcatUtils]::GetDesiredServerSettings - The Java version ($javaVersion) is not supported by the server settings.")
            $unsupportedVersion = $true
        }
        if ([System.Version] $_settings.TomcatMinVersion -gt [System.Version] $tomcatVersion) {
            [DTXLogger]::LogWarning("[DTXTomcatUtils]::GetDesiredServerSettings - The Tomcat version ($tomcatVersion) is not supported by the server settings.")
            $unsupportedVersion = $true
        }
        if ($unsupportedVersion) {
            throw "[DTXTomcatUtils]::GetDesiredServerSettings - Unable to find server settings for the current Tomcat and/or Java versions."
        }
        return $_settings
    }
}
class DTXTomcatServerXml {
    hidden [string]$className = $this.GetType().Name
    hidden [System.Xml.XmlDocument] $serverXmlContents
    hidden [string] $serverXmlPath
    DTXTomcatServerXml([string]$Path) {
        if (-not (Test-Path -Path $Path)) {
            [DTXLogger]::LogCriticalAndThrow("[$($this.className)]::new - The server.xml file at '$Path' does not exist.")
        }
        $this.serverXmlPath = $Path
        $this.Reload()
    }
    [System.Xml.XmlDocument] GetContents() {
        return $this.serverXmlContents
    }
    [System.Xml.XmlElement[]] GetConnectorElements() {
        return $this.serverXmlContents.Server.Service.Connector
    }
    [System.Xml.XmlElement] GetConnectorElement([int]$ConnectorPort) {
        $connectorElement = $this.GetConnectorElements() | Where-Object { $_.Port -eq $ConnectorPort }
        if ($null -eq $connectorElement) {
            [DTXLogger]::LogCriticalAndThrow("[$($this.className)]::GetConnectorElement - Unable to find the connector node for port $ConnectorPort in the server.xml file.")
        }
        return $connectorElement
    }
    [System.Xml.XmlElement] GetConnectorElementWithSslEnabled() {
        $sslConnector = $this.GetConnectorElements() | Where-Object { ($_.Attributes["SSLEnabled"].Value -eq "true") }
        if ($null -eq $sslConnector) {
            return $null
        }
        if (($sslConnector -is [System.Collections.IEnumerable]) -and ($sslConnector.Count -gt 1)) {
            [DTXLogger]::LogCriticalAndThrow("[$($this.className)]::GetConnectorElementWithSslEnabled - Multiple SSL enabled connectors found in the server.xml file.")
        }
        return $sslConnector
    }
    [void] RemoveConnectorElement([int]$ConnectorPort) {
        $connectorElement = $this.GetConnectorElement($ConnectorPort)
        $connectorElement.ParentNode.RemoveChild($connectorElement)
    }
    [string] GetConnectorProperty([int]$ConnectorPort, [string]$PropName) {
        $connectorElement = $this.GetConnectorElement($ConnectorPort)
        $propValue = $null
        if ($null -ne $connectorElement) {
            $normalizedPropName = $PropName.ToLower()
            $existingAttr = $null
            foreach ($attr in $connectorElement.Attributes) {
                if ($attr.Name.ToLower() -eq $normalizedPropName) {
                    $existingAttr = $attr
                    break
                }
            }
            if ($null -ne $existingAttr) {
                $propValue = $existingAttr.Value
            }
            else {
                [DTXLogger]::LogWarning("[$($this.className)]::GetConnectorProperty - Unable to find the property '$PropName' for the connector on port $ConnectorPort.")
            }
        }
        return $propValue
    }
    [void] SetConnectorProperty([int]$ConnectorPort, [string]$PropName, [string]$PropValue) {
        $connectorElement = $this.GetConnectorElement($ConnectorPort)
        if ($null -ne $connectorElement ) {
            $normalizedPropName = $PropName.ToLower()
            $existingAttr = $null
            foreach ($attr in $connectorElement.Attributes) {
                if ($attr.Name.ToLower() -eq $normalizedPropName) {
                    $existingAttr = $attr
                    break
                }
            }
            if ($null -ne $existingAttr) {
                $existingAttr.Value = $PropValue
            }
            else {
                $newAttr = $connectorElement.OwnerDocument.CreateAttribute($PropName)
                $newAttr.Value = $PropValue
                $connectorElement.Attributes.Append($newAttr)
            }
        }
    }
    [void] RemoveConnectorProperty([int] $ConnectorPort, [string] $PropName) {
        $connectorElement = $this.GetConnectorElement($ConnectorPort)
        if ($null -ne $connectorElement) {
            $normalizedPropName = $PropName.ToLower()
            $existingAttr = $null
            foreach ($attr in $connectorElement.Attributes) {
                if ($attr.Name.ToLower() -eq $normalizedPropName) {
                    $existingAttr = $attr
                    break
                }
            }
            if ($null -ne $existingAttr) {
                $connectorElement.Attributes.Remove($existingAttr)
            }
        }
    }
    [bool] GetConnectorDnsLookupsEnabled([int] $ConnectorPort) {
        return ($this.GetConnectorProperty($ConnectorPort, "enableLookups") -eq "true")
    }
    [void] SetConnectorDnsLookups([int] $ConnectorPort, [bool] $EnableLookups) {
        $this.SetConnectorProperty($ConnectorPort, "enableLookups", $EnableLookups.ToString().ToLower())
    }
    [string] GetConnectorSslEnabledProtocols([int] $ConnectorPort) {
        return $this.GetConnectorProperty($ConnectorPort, "sslEnabledProtocols")
    }
    [void] SetConnectorSslEnabledProtocols([int] $ConnectorPort, [string] $Protocols) {
        $this.SetConnectorProperty($ConnectorPort, "sslEnabledProtocols", $Protocols)
    }
    [string] GetConnectorSslCiphers([int] $ConnectorPort) {
        return $this.GetConnectorProperty($ConnectorPort, "ciphers")
    }
    [void] SetConnectorSslCiphers([int] $ConnectorPort, [string] $Ciphers) {
        $this.SetConnectorProperty($ConnectorPort, "ciphers", $Ciphers)
    }
    [bool] IsConnectorHttp2Enabled([int] $ConnectorPort) {
        $connectorElement = $this.GetConnectorElement($ConnectorPort)
        $upgradeProtocolElement = $connectorElement.SelectSingleNode("UpgradeProtocol")
        if ($null -eq $upgradeProtocolElement) {
            return $false
        }
        $classNameValue = $upgradeProtocolElement.GetAttribute("className")
        return ($classNameValue -eq "org.apache.coyote.http2.Http2Protocol")
    }
    [void] SetConnectorHttp2([int] $ConnectorPort, [bool] $EnableHttp2) {
        $connectorElement = $this.GetConnectorElement($ConnectorPort)
        if ($EnableHttp2) {
            $upgradeProtocolElement = $connectorElement.SelectSingleNode("UpgradeProtocol")
            if ($null -eq $upgradeProtocolElement) {
                $upgradeProtocolElement = $connectorElement.OwnerDocument.CreateElement("UpgradeProtocol")
                $upgradeProtocolElement.SetAttribute("className", "org.apache.coyote.http2.Http2Protocol")
                $connectorElement.AppendChild($upgradeProtocolElement)
            }
            else {
                $upgradeProtocolElement.SetAttribute("className", "org.apache.coyote.http2.Http2Protocol")
            }
        }
        else {
            $upgradeProtocolElement = $connectorElement.SelectSingleNode("UpgradeProtocol")
            if ($null -ne $upgradeProtocolElement) {
                $connectorElement.RemoveChild($upgradeProtocolElement)
            }
        }
        $this.RemoveConnectorProperty($ConnectorPort, "upgradeProtocol")
    }
    [string] GetConnectorKeystoreFilePath([int] $ConnectorPort) {
        return $this.GetConnectorProperty($ConnectorPort, "keystoreFile")
    }
    [void] SetConnectorKeystoreFilePath([int] $ConnectorPort, [string] $KeystoreFilePath) {
        if (-not (Test-Path -Path $KeystoreFilePath)) {
            [DTXLogger]::LogCriticalAndThrow("[DTXTomcatServerXml]::SetConnectorKeystoreFilePath - The key store file path '$KeystoreFilePath' is invalid.")
        }
        $this.SetConnectorProperty($ConnectorPort, "keystoreFile", $KeystoreFilePath)
    }
    [string] GetServerInfoStatus() {
        $_node = $this.serverXmlContents.Server.Service.Engine.Host.SelectSingleNode("Valve[@className='org.apache.catalina.valves.ErrorReportValve']")
        if ($_node) {
            $showServerInfoAttribute = $_node.GetAttribute("showServerInfo")
            if ($showServerInfoAttribute -eq "true") {
                return "true"
            }
            if ($showServerInfoAttribute -eq "false") {
                return "false"
            }
        }
        return "not configured"
    }
    [bool] IsServerInfoEnabled() {
        return ($this.GetServerInfoStatus() -eq "true")
    }
    [void] SetServerInfo([bool] $EnableServerInfo) {
        $showServerInfoValue = if ($EnableServerInfo) { "true" } else { "false" }
        $_node = $this.serverXmlContents.Server.Service.Engine.Host.SelectSingleNode("Valve[@className='org.apache.catalina.valves.ErrorReportValve']")
        if ($_node) {
            $_node.SetAttribute("showServerInfo", $showServerInfoValue)
        }
        else {
            $_newNode = $this.serverXmlContents.CreateElement("Valve")
            $_newNode.SetAttribute("className", "org.apache.catalina.valves.ErrorReportValve")
            $_newNode.SetAttribute("showServerInfo", $showServerInfoValue)
            $this.serverXmlContents.Server.Service.Engine.Host.AppendChild($_newNode) | Out-Null
        }
    }
    [string] GetErrorReportStatus() {
        $_node = $this.serverXmlContents.Server.Service.Engine.Host.SelectSingleNode("Valve[@className='org.apache.catalina.valves.ErrorReportValve']")
        if ($_node) {
            $showReportAttribute = $_node.GetAttribute("showReport")
            if ($showReportAttribute -eq "true") {
                return "true"
            }
            if ($showReportAttribute -eq "false") {
                return "false"
            }
        }
        return "not configured"
    }
    [bool] IsErrorReportEnabled() {
        return ($this.GetErrorReportStatus() -eq "true")
    }
    [void] SetErrorReports([bool] $EnableErrorReports) {
        $showReportValue = if ($EnableErrorReports) { "true" } else { "false" }
        $_node = $this.serverXmlContents.Server.Service.Engine.Host.SelectSingleNode("Valve[@className='org.apache.catalina.valves.ErrorReportValve']")
        if ($_node) {
            $_node.SetAttribute("showReport", $showReportValue)
        }
        else {
            $_newNode = $this.serverXmlContents.CreateElement("Valve")
            $_newNode.SetAttribute("className", "org.apache.catalina.valves.ErrorReportValve")
            $_newNode.SetAttribute("showReport", $showReportValue)
            $this.serverXmlContents.Server.Service.Engine.Host.AppendChild($_newNode) | Out-Null
        }
    }
    [void] CleanUp() {
        $allUpgradeProtocolElements = $this.serverXmlContents.SelectNodes("//UpgradeProtocol")
        foreach ($element in $allUpgradeProtocolElements) {
            if ($element.ParentNode.Name -ne "Connector") {
                $element.ParentNode.RemoveChild($element)
            }
        }
    }
    [void] Reload() {
        $this.serverXmlContents = $null
        try {
            $this.serverXmlContents = [xml](Get-Content -Path $this.serverXmlPath -ErrorAction Stop)
            [DTXLogger]::LogInfo("[$($this.className)]::Reload - Loaded the server.xml file from $($this.serverXmlPath)")
        }
        catch {
            [DTXLogger]::LogCriticalAndThrow("[$($this.className)]::Reload - Unable to read the server.xml file at $($this.serverXmlPath)", $_)
        }
    }
    [void] Save() {
        $this.CleanUp()
        $formattedServerXmlContents = [DTXXmlUtils]::FormatXml($this.serverXmlContents)
        [DTXUtils]::WriteStringToFileWithRetry($formattedServerXmlContents, $this.serverXmlPath)
        $this.Reload()
    }
}
class DTXTomcatChangeEngineSettings {
    hidden [string] $className = $this.GetType().Name
    hidden [string] $dtxStateStatusKey = "tomcat-changes-$($this.className.ToLower())-status"
    hidden [boolean] $isChangeRequired = $false
    hidden [hashtable] $dtxSystemInfo
    hidden [hashtable] $dtxDefaults
    hidden [string] $tomcatHostname = "localhost"
    hidden [int]$tomcatHttpPort
    hidden [int] $settingsVersion
    hidden [hashtable] $desiredServerSettings
    DTXTomcatChangeEngineSettings(
        [Hashtable]$DTXSystemInfo,
        [Hashtable]$DTXDefaults,
        [int] $SettingsVersion
    ) {
        [DTXStateProvider]::SetValue($this.dtxStateStatusKey, "UNKNOWN")
        $this.dtxSystemInfo = $DTXSystemInfo
        $this.dtxDefaults = $DTXDefaults
        $this.tomcatHttpPort = [int]($this.dtxSystemInfo.AppInfo.Tomcat.ServerXML.Port)
        $this.settingsVersion = $SettingsVersion
        $this.desiredServerSettings = [DTXTomcatUtils]::GetDesiredServerSettings($this.dtxSystemInfo, $this.dtxDefaults, $this.settingsVersion)
    }
    hidden [bool] IsTomcatEngineChangeRequired() {
        $this.isChangeRequired = $false
        $tomcatServerXml = [DTXTomcatServerXml]::new($this.dtxSystemInfo.AppInfo.Tomcat.ServerXML.Path)
        $desiredShowServerInfo = $this.desiredServerSettings.ShowServerInfo
        $currentShowServerInfo = $tomcatServerXml.GetServerInfoStatus()
        if ($currentShowServerInfo -eq "not configured") {
            Write-LogWarning -Message "[$($this.className)] The current Show Server Info setting is not configured. Setting to desired: $desiredShowServerInfo."
            $this.isChangeRequired = $true
        }
        if ($currentShowServerInfo -eq "true" -and $desiredShowServerInfo -ne $true) {
            Write-LogWarning -Message "[$($this.className)] The current Show Server Info setting is true, but desired is $desiredShowServerInfo. Update required."
            $this.isChangeRequired = $true
        }
        if ($currentShowServerInfo -eq "false" -and $desiredShowServerInfo -ne $false) {
            Write-LogWarning -Message "[$($this.className)] The current Show Server Info setting is false, but desired is $desiredShowServerInfo. Update required."
            $this.isChangeRequired = $true
        }
        $desiredShowErrorReports = $this.desiredServerSettings.ShowErrorReports
        $currentShowErrorReports = $tomcatServerXml.GetErrorReportStatus()
        if ($currentShowErrorReports -eq "not configured") {
            Write-LogWarning -Message "[$($this.className)] The current Show Error Reports setting is not configured. Setting to desired: $desiredShowErrorReports."
            $this.isChangeRequired = $true
        }
        if ($currentShowErrorReports -eq "true" -and $desiredShowErrorReports -ne $true) {
            Write-LogWarning -Message "[$($this.className)] The current Show Error Reports setting is true, but desired is $desiredShowErrorReports. Update required."
            $this.isChangeRequired = $true
        }
        if ($currentShowErrorReports -eq "false" -and $desiredShowErrorReports -ne $false) {
            Write-LogWarning -Message "[$($this.className)] The current Show Error Reports setting is false, but desired is $desiredShowErrorReports. Update required."
            $this.isChangeRequired = $true
        }
        return $this.isChangeRequired
    }
    [bool] ChangeRequired() { 
        if ($this.IsTomcatEngineChangeRequired()) {
            [DTXStateProvider]::SetValue($this.dtxStateStatusKey, "CHANGE_REQUIRED")
            return $true
        }
        [DTXStateProvider]::SetValue($this.dtxStateStatusKey, "CHANGE_SUCCESSFUL")
        return $false
    }
    [void] PerformChange() {
        if ($this.isChangeRequired) {
            $tomcatServerXml = [DTXTomcatServerXml]::new($this.dtxSystemInfo.AppInfo.Tomcat.ServerXML.Path)
            $tomcatServerXml.SetServerInfo($this.desiredServerSettings.ShowServerInfo)
            $tomcatServerXml.SetErrorReports($this.desiredServerSettings.ShowErrorReports)
            $tomcatServerXml.Save()
        }
    }
    [void] TestAfterChange() {
        if (-not ($this.ChangeRequired())) {
            [DTXLogger]::LogInfo("[$($this.className)] The Tomcat engine settings change was successful.")
            return
        }
        [DTXStateProvider]::SetValue($this.dtxStateStatusKey, "CHANGE_FAILED")
        [DTXLogger]::LogCriticalAndThrow("[$($this.className)] The Tomcat engine settings change failed.")
    }
}
class DTXTomcatChangeConnectorSettings {
    hidden [string] $className = $this.GetType().Name
    hidden [string] $dtxStateStatusKey = "tomcat-changes-$($this.className.ToLower())-status"
    hidden [boolean] $isChangeRequired = $false
    hidden [hashtable] $dtxSystemInfo
    hidden [hashtable] $dtxDefaults
    hidden [string] $tomcatHostname = "localhost"
    hidden [int]$tomcatHttpPort
    hidden [int] $settingsVersion
    hidden [hashtable] $desiredServerSettings
    DTXTomcatChangeConnectorSettings(
        [Hashtable]$DTXSystemInfo,
        [Hashtable]$DTXDefaults,
        [int] $SettingsVersion
    ) {
        [DTXStateProvider]::SetValue($this.dtxStateStatusKey, "UNKNOWN")
        $this.dtxSystemInfo = $DTXSystemInfo
        $this.dtxDefaults = $DTXDefaults
        $this.tomcatHttpPort = [int]($this.dtxSystemInfo.AppInfo.Tomcat.ServerXML.Port)
        $this.settingsVersion = $SettingsVersion
        $this.desiredServerSettings = [DTXTomcatUtils]::GetDesiredServerSettings($this.dtxSystemInfo, $this.dtxDefaults, $this.settingsVersion)
    }
    [boolean] GetOverrideEnableHttp2Value([boolean]$defaultValue) {
        $desiredValue = $defaultValue
        if ($this.desiredServerSettings.ContainsKey("OverrideEnableHTTP2")) {
            $instanceId = $this.dtxSystemInfo.GenericInfo.IdentityInfo.instanceId
            if ($this.desiredServerSettings.OverrideEnableHTTP2.ContainsKey($instanceId)) {
                Write-LogWarning -Message "[$($this.className)] The EnableHTTP2 setting is overridden for instance $instanceId."
                $desiredValue = $this.desiredServerSettings.OverrideEnableHTTP2[$instanceId]
            }
        }
        return $desiredValue
    }
    [bool] IsTomcatConnectorChangeRequired() {
        $this.isChangeRequired = $false
        $tomcatServerXml = [DTXTomcatServerXml]::new($this.dtxSystemInfo.AppInfo.Tomcat.ServerXML.Path)
        $desiredDnsLookpsValue = $this.desiredServerSettings.EnableDnsLookups
        $currentDnsLookupsValue = $tomcatServerXml.GetConnectorDnsLookupsEnabled($this.tomcatHttpPort)
        if ($desiredDnsLookpsValue -ne $currentDnsLookupsValue) {
            Write-LogWarning -Message "[$($this.className)] The desired DNS Lookups setting is $desiredDnsLookpsValue. The current setting is $currentDnsLookupsValue."
            $this.isChangeRequired = $true
        }
        $desiredSslEnabledProtocols = $this.desiredServerSettings.SSLSettings.Protocols -join ","
        $currentSslEnabledProtocols = $tomcatServerXml.GetConnectorSslEnabledProtocols($this.tomcatHttpPort)
        if ($desiredSslEnabledProtocols -ne $currentSslEnabledProtocols) {
            Write-LogWarning -Message "[$($this.className)] The desired SSL Enabled Protocols setting is $desiredSslEnabledProtocols. The current setting is $currentSslEnabledProtocols."
            $this.isChangeRequired = $true
        }
        $desiredSslCiphers = $this.desiredServerSettings.SSLSettings.Ciphers
        $currentSslCiphers = $tomcatServerXml.GetConnectorSslCiphers($this.tomcatHttpPort)
        if ($desiredSslCiphers -ne $currentSslCiphers) {
            Write-LogWarning -Message "[$($this.className)] The desired SSL Ciphers setting is $desiredSslCiphers. The current setting is $currentSslCiphers."
            $this.isChangeRequired = $true
        }
        $desiredHttp2Enabled = $this.GetOverrideEnableHttp2Value($this.desiredServerSettings.EnableHTTP2)
        $currentHttp2Enabled = $tomcatServerXml.IsConnectorHttp2Enabled($this.tomcatHttpPort)
        if ($desiredHttp2Enabled -ne $currentHttp2Enabled) {
            Write-LogWarning -Message "[$($this.className)] The desired HTTP2 setting is $desiredHttp2Enabled. The current setting is $currentHttp2Enabled."
            $this.isChangeRequired = $true
        }
        return $this.isChangeRequired
    }
    [bool] ChangeRequired() {
        if ($this.IsTomcatConnectorChangeRequired()) {
            [DTXStateProvider]::SetValue($this.dtxStateStatusKey, "CHANGE_REQUIRED")
            return $true
        }
        [DTXStateProvider]::SetValue($this.dtxStateStatusKey, "CHANGE_SUCCESSFUL")
        return $false 
    }
    [void] PerformChange() {
        if ($this.isChangeRequired) {
            $tomcatServerXml = [DTXTomcatServerXml]::new($this.dtxSystemInfo.AppInfo.Tomcat.ServerXML.Path)
            $tomcatServerXml.SetConnectorDnsLookups($this.tomcatHttpPort, $this.desiredServerSettings.EnableDnsLookups)
            $tomcatServerXml.SetConnectorSslEnabledProtocols($this.tomcatHttpPort, ($this.desiredServerSettings.SSLSettings.Protocols -join ","))
            $tomcatServerXml.SetConnectorSslCiphers($this.tomcatHttpPort, $this.desiredServerSettings.SSLSettings.Ciphers)
            $desiredHttp2Enabled = $this.GetOverrideEnableHttp2Value($this.desiredServerSettings.EnableHTTP2)
            $tomcatServerXml.SetConnectorHttp2($this.tomcatHttpPort, $desiredHttp2Enabled)
            $tomcatServerXml.Save()
        }
    }
    [void] TestAfterChange() {
        if (-not ($this.ChangeRequired())) {
            [DTXLogger]::LogInfo("[$($this.className)] The Tomcat connector settings change was successful.")
            return
        }
        [DTXStateProvider]::SetValue($this.dtxStateStatusKey, "CHANGE_FAILED")
        [DTXLogger]::LogCriticalAndThrow("[$($this.className)] The Tomcat connector settings change failed.")
    }
}
class DTXTomcatChangeCertificate {
    hidden [string] $className = $this.GetType().Name
    hidden [string] $dtxStateStatusKey = "tomcat-changes-$($this.className.ToLower())-status"
    hidden [hashtable] $dtxSystemInfo
    hidden [hashtable] $dtxDefaults
    hidden [boolean] $isTomcatServerXmlUpdateRequired = $false
    hidden [string] $certNamespace = "star.dotmatics.net"
    hidden [string] $certPath
    hidden [string] $tomcatHostname = "localhost"
    hidden [int] $tomcatHttpPort
    hidden [DTXTomcatServerXml] $tomcatServerXml
    DTXTomcatChangeCertificate(
        [Hashtable]$DTXSystemInfo,
        [Hashtable]$DTXDefaults
    ) {
        [DTXStateProvider]::SetValue($this.dtxStateStatusKey, "UNKNOWN")
        $this.dtxSystemInfo = $DTXSystemInfo
        $this.dtxDefaults = $DTXDefaults
        $this.tomcatHttpPort = [int]($this.dtxSystemInfo.AppInfo.Tomcat.ServerXML.Port)
        $this.tomcatServerXml = [DTXTomcatServerXml]::new($this.dtxSystemInfo.AppInfo.Tomcat.ServerXML.Path)
        $this.certPath = Join-Path ($this.dtxSystemInfo.StateInfo["certificates-[$($this.certNamespace)]-path"]) "$($this.certNamespace).jks"
    }
    [bool] IsTomcatCertChangeRequired() {
        $liveCertFingerprint = $null
        $installedCertFingerprint = $null
        try {
            Wait-ForWebServerToServeRequests -Hostname $this.tomcatHostname -Port $this.tomcatHttpPort -WaitSeconds 30 -SkipCertificateCheck
            $liveCertFingerprint = (Get-SSLCertificateFingerprint -Hostname $this.tomcatHostname -Port $this.tomcatHttpPort -HashAlgorithm "SHA1").ToLower()
        }
        catch {
            [DTXLogger]::LogCriticalAndThrow("[$($this.className)] Unable to obtain the live certificate fingerprint from Tomcat. Is the Tomcat service running?", $_.Exception)
        }
        try {
            $installedCertFingerprint = ($this.dtxSystemInfo.StateInfo["certificates-[$($this.certNamespace)]-fingerprint-sha1"]).ToLower()
        }
        catch {
            [DTXLogger]::LogCriticalAndThrow("[$($this.className)] The fingerprint for the Tomcat certificate is not set in the state file. Did the Sync-StarDotmaticsNetCertificate cmdlet run successfully?", $_.Exception)
        }
        if ($liveCertFingerprint -ne $installedCertFingerprint) {
            Write-LogInfo -Message "[$($this.className)] The live certificate fingerprint ($liveCertFingerprint) does not match the installed certificate fingerprint ($installedCertFingerprint)."
            return $true
        }
        return $false
    }
    [bool] IsTomcatServerXmlChangeRequired () {
        $configuredCertPath = $this.tomcatServerXml.GetConnectorKeystoreFilePath($this.tomcatHttpPort)
        if ([System.IO.Path]::GetFullPath($configuredCertPath) -ne [System.IO.Path]::GetFullPath($this.certPath)) {
            Write-LogInfo -Message "[$($this.className)] The configured keystore path ($configuredCertPath) does not match the desired keystore path $($this.certPath)."
            $this.isTomcatServerXmlUpdateRequired = $true
            return $true
        }
        return $false
    }
    [void] UpdateTomcatServerXmlFile() {
        $this.tomcatServerXml.Reload() 
        $this.tomcatServerXml.SetConnectorKeystoreFilePath($this.tomcatHttpPort, $this.certPath) 
        $this.tomcatServerXml.Save() 
    }
    [bool] ChangeRequired() {
        $isTomcatCertChangeRequired = $this.IsTomcatCertChangeRequired()
        $isTomcatServerXmlChangeRequired = $this.IsTomcatServerXmlChangeRequired()
        if ($isTomcatCertChangeRequired -or $isTomcatServerXmlChangeRequired) {
            [DTXStateProvider]::SetValue($this.dtxStateStatusKey, "CHANGE_REQUIRED")
            return $true
        }
        [DTXStateProvider]::SetValue($this.dtxStateStatusKey, "CHANGE_SUCCESSFUL")
        return $false
    }
    [void] PerformChange() {
        if ($this.isTomcatServerXmlUpdateRequired) {
            $this.UpdateTomcatServerXmlFile()
        }
    }
    [void] TestAfterChange() {
        if (-not ($this.ChangeRequired())) {
            [DTXLogger]::LogInfo("[$($this.className)] The Tomcat certificate change was successful.")
            return
        }
        [DTXStateProvider]::SetValue($this.dtxStateStatusKey, "CHANGE_FAILED")
        [DTXLogger]::LogCriticalAndThrow("[$($this.className)] The Tomcat certificate change failed.")
    }
}
function Add-InstanceLocalGroupMember {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [String]
        $InstanceId,
        [Parameter(Mandatory = $true)]
        [String]
        $LocalGroup,
        [Parameter(Mandatory = $true)]
        [String]
        $Member,
        [Parameter(Mandatory = $true)]
        [String]
        $Region
    )
    process {
        try {
            Test-SSMReachability -InstanceId $InstanceId -Region $Region -SkipCommandExecution -ThrowException
            $documentParams = @{
                LocalGroupName = $LocalGroup
                GroupMember    = $Member
            }
            $invokeParams = @{
                Name       = "DTX-AddInstanceLocalGroupMember"
                Region     = $Region
                InstanceId = $InstanceId
                Parameters = $documentParams
            }
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Adding '$Member' as a group member to local instance group '$LocalGroup' on instance '$InstanceId'."
            $commandId = (Invoke-SSMDocumentAndRetry @invokeParams).CommandId
            Test-SSMCommandResultV2 -CommandId $commandId -InstanceId $InstanceId -Region $Region -Wait -ThrowException | Out-Null
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Operation succeeded."
        }
        catch {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] $( $_.Exception.Message )"  -ThrowException -Exception $_
        }
    }
}
function Assert-True {
  [CmdletBinding()]
  param(
    [Parameter(Position = 0)]
    [object]
    $Condition,
    [Parameter(Position = 1)]
    [string]
    $Message
  )
  Set-StrictMode -Version 'Latest'
  if ( -not $condition ) {
    throw (New-Object DTXPSException("[$( $MyInvocation.MyCommand )] Expected true but was false: $message"))
  }
}
function Backup-OpenEye {
    try {
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Starting an Open Eye backup procedure..."
        $gifCachePath = Get-BrowserPlatformGifcachePath
        Push-Location -Path $gifCachePath
        $toolkitPath = Get-BrowserPlatformGifcacheToolkitPath
        if ((Get-ChildItem -Path $toolkitPath).Count -eq 0) {
            Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] The toolkit path '$toolkitPath' is empty. Skipping taking a backup."
            return $null
        }
        $backupFilePath = Join-Path $gifCachePath "toolkit.bak.$(Get-Date -Format "yyyy-MM-dd_hh-mm-ss").zip"
        $rootFiles = Get-ChildItem -path $toolkitPath -File
        $totalSizeInMB=0
        foreach($topLevelFile in $rootFiles){
            $totalSizeInMB+=$topLevelFile.Length / 1MB
        }
        if ($totalSizeInMB -gt 1024){
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] Size of files too large to perform a backup, please investigate. Total size(MB): $($totalSizeInMb)" -ThrowException 
        }
        $rootFiles | Compress-Archive -CompressionLevel NoCompression -DestinationPath $backupFilePath
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Backup file saved at: $($backupFilePath)"
        return Get-Item -Path $backupFilePath
    }
    finally {
        Pop-Location
    }
}
function Confirm-AWSAMI {
    [cmdletbinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]$Id,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]$Region
    )
    process {
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Validating AMI..."
        try {
            [void]$( Get-EC2Image -ImageId $Id -Region $Region )
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Validation complete!"
        }
        catch [InvalidOperationException] {
            if ($_.Exception.Message -like "*does not exist*") {
                Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] The AMI id '$Id' does not exist in the '$Region' region."  -ThrowException -Exception $_
            }
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] $( $_.Exception.Message )"  -ThrowException -Exception $_
        }
    }
}
function Confirm-AWSCredentials {
    [cmdletbinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]$AWSAccountNumber,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]$Region
    )
    process {
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Validating AWS credentials..."
        try {
            $currentCallerIdentity = Get-STSCallerIdentity -Region $Region
            if ($currentCallerIdentity.Account -eq $AWSAccountNumber) {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Validation complete!"
            }
            else {
                Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] Your AWS credentials have access to AWS account $( $currentCallerIdentity.Account ) instead of the requested account $AWSAccountNumber"
                throw
            }
        }
        catch {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] $( $_.Exception.Message )"  -ThrowException -Exception $_
            throw
        }
    }
}
function Confirm-AWSEC2InstanceQuota {
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]$InstanceType,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]$Region
    )
    process {
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Checking the AWS EC2 vCPU quota limit..."
        try {
            if (@("a", "c", "d", "h", "i", "m", "r", "t", "z") -contains $InstanceType.ToLower()[0]) {
                $serviceLimit = (Get-SQServiceQuota -QuotaCode "L-1216C47A" -ServiceCode ec2 -Region $Region).Value
                [Amazon.CloudWatch.Model.Dimension[]]$dimensions = @()
                $d1 = New-Object Amazon.CloudWatch.Model.Dimension
                $d1.Name = "Service"
                $d1.Value = "EC2"
                $d2 = New-Object Amazon.CloudWatch.Model.Dimension
                $d2.Name = "Type"
                $d2.Value = "Resource"
                $d3 = New-Object Amazon.CloudWatch.Model.Dimension
                $d3.Name = "Class"
                $d3.Value = "Standard/OnDemand"
                $d4 = New-Object Amazon.CloudWatch.Model.Dimension
                $d4.Name = "Resource"
                $d4.Value = "vCPU"
                [Amazon.CloudWatch.Model.Dimension[]]$dimensions = @($d1, $d2, $d3, $d4)
                $cwMetricStatsParams = @{
                    MetricName   = "ResourceCount"
                    Namespace    = "AWS/Usage"
                    Statistic    = "Maximum"
                    Dimension    = $dimensions
                    Period       = 3600
                    UtcStartTime = (Get-Date).ToUniversalTime().AddHours(-2)
                    UtcEndTime   = (Get-Date).ToUniversalTime()
                }
                $vCPUCount = (Get-CWMetricStatistic @cwMetricStatsParams -Region $Region).Datapoints[0].Maximum
                if ($vCPUCount -ge $serviceLimit) {
                    Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] The EC2 vCPU quota limit is reached in the '$Region' region. Please request an increase before proceeding."
                    throw
                }
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] EC2 vCPU quota: $serviceLimit"
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] EC2 vCPU used: $vCPUCount"
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] EC2 vCPU remaining: $( $serviceLimit - $vCPUCount )"
            }
        }
        catch {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] $( $_.Exception.Message )" -ThrowException -Exception $_
        }
    }
}
function Confirm-AWSElasticIPQuota {
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]$Region
    )
    process {
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Checking the AWS Elastic IP quota limit..."
        try {
            $elasticIPCount = (Get-EC2Address -Filter @{ Name = "domain"; Values = "vpc" } -Region $Region).Count
            $serviceQuotaLimit = (Get-SQServiceQuota -QuotaCode "L-0263D0A3" -ServiceCode ec2 -Region $Region).Value
            if ($elasticIPCount -ge $serviceQuotaLimit) {
                Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] The Elastic IP quota limit is reached in the '$Region' region. Please request an increase before proceeding."
                throw
            }
            $elasticIPremaining = ($serviceQuotaLimit - $elasticIPCount)
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Elastic IPs quota: $serviceQuotaLimit"
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Elastic IPs used: $elasticIPCount"
            if ($elasticIPremaining -lt 5) {
                Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Elastic IPs remaining: $( $serviceQuotaLimit - $elasticIPCount )"
            }
            else {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Elastic IPs remaining: $( $serviceQuotaLimit - $elasticIPCount )"
            }
        }
        catch {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] $( $_.Exception.Message )" -ThrowException -Exception $_
        }
    }
}
function Confirm-AWSIAMRole {
    [cmdletbinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]$RoleName,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]$Region,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidatePattern('^\d{12}$')]
        [String]$AWSAccountNumber
    )
    process {
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Validating that the IAM Role exists in account: $AWSAccountNumber..."
        try {
            $iamRoleExists = Get-IAMRole -RoleName $RoleName -Region "us-east-1"
            if ($iamRoleExists.Arn -notlike "*" + $AWSAccountNumber + "*") {
                Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] IAM role $RoleName does not exist in account: $AWSAccountNumber..."
                throw
            }
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] IAM role $RoleName exists in account: $AWSAccountNumber..."
        }
        catch {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] $( $_.Exception.Message )" -ThrowException -Exception $_
        }
    }
}
function Confirm-AWSSubnet {
    [cmdletbinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]$SubnetId,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]$VPCId,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]$Region
    )
    process {
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Validating VPC Subnet..."
        try {
            $subnet = Get-EC2Subnet -SubnetId $SubnetId -Region $Region
            if ($subnet.VpcId -ne $VPCId) {
                Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] Validation failed. Subnet '$SubnetId' does not belong to VPC '$VPCId'."
                throw
            }
            if ($subnet.AvailableIpAddressCount -lt 1) {
                Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] Validation failed. Subnet '$SubnetId' does not have enough IP addresses left."
                throw
            }
            if ($subnet.AvailableIpAddressCount -lt 10) {
                Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Available IP addreses in subnet '$SubnetId': $( $subnet.AvailableIpAddressCount )"
            }
            else {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Available IP addreses in subnet '$SubnetId': $( $subnet.AvailableIpAddressCount )"
            }
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Validation complete!"
        }
        catch {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] $( $_.Exception.Message )" -ThrowException -Exception $_
        }
    }
}
function Confirm-AWSVPC {
    [cmdletbinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]$VPCId,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]$Region
    )
    process {
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Validating VPC..."
        try {
            [void](Get-EC2Vpc -VpcId $VPCId -Region $Region)
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Validation complete!"
        }
        catch {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] $( $_.Exception.Message )" -ThrowException -Exception $_
        }
    }
}
function Confirm-ComputerName {
    param (
        [Parameter(Mandatory = $true)]
        [String]$ComputerName,
        [Parameter(Mandatory = $true)]
        [String]$DomainControllerInstanceId,
        [Parameter(Mandatory = $true)]
        [String]$DomainControllerRegion
    )
    process {
        try {
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Confirming computer name '$ComputerName' is available."
            $documentParams = @{
                ComputerName = $ComputerName
            }
            $invokeParams = @{
                Name       = "DTX-ConfirmComputerName"
                Region     = $DomainControllerRegion
                InstanceId = $DomainControllerInstanceId
                Parameters = $documentParams
            }
            $command = Invoke-SSMDocumentAndRetry @invokeParams
            $commandOutputParams = @{
                CommandId  = $command.CommandId
                InstanceId = $DomainControllerInstanceId
                Region     = $DomainControllerRegion
            }
            $commandOutput = Get-SSMCommandOutput @commandOutputParams
            $message = ($commandOutput.ConfirmComputerName.StandardOutput | ConvertFrom-Json).Message
            if ($message -eq "NotFound") {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Computer name $ComputerName is available."
                return
            }
            if (Read-BasicUserResponse -Prompt "The computer name '$ComputerName' is already in use. If you are running this script for the first time, you most likely need to stop here and investigate why the instance name exists already. Or rerun the script to generate a new random suffix. Would you like to continue? (y/n)") {
                Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Computer name '$ComputerName' is already in use."
                return
            }
            else {
                Write-LogCritical -ThrowException -Message "[$( $MyInvocation.MyCommand )] Computer name '$ComputerName' is already in use."
            }
        } 
        catch {
            Write-LogCritical -ThrowException -Message "[$( $MyInvocation.MyCommand )] Unable to confirm computer name '$ComputerName' is available. Error message: $( $_.Exception.Message )" -Exception $_
        }
    }
}
function Confirm-InstanceName {
    param (
        [Parameter(Mandatory = $true)]
        [String]$InstanceName,
        [Parameter(Mandatory = $true)]
        [String]$Region
    )
    process {
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Confirming instance name '$InstanceName' is available."
        $instance = Get-EC2Instance -Region $Region -Filter @{Name = "tag:Name"; Values = "$InstanceName" }
        if ($instance) {
            if (Read-BasicUserResponse -Prompt "The instance name '$InstanceName' is already in use. If you are running this script for the first time, you most likely need to stop here and investigate why the instance name exists already. Or rerun the script to generate a new random suffix. Would you like to continue? (y/n)") {
                Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Instance name '$InstanceName' is already in use."
                return
            }
            else {
                Write-LogCritical -ThrowException -Message "[$( $MyInvocation.MyCommand )] Instance name '$InstanceName' is already in use."
            }
        }
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Instance name '$InstanceName' is available."
    }
}
function Confirm-PRTGConnection {
    [cmdletbinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]$Username,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]$Password,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]$Hostname
    )
    process {
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Validating PRTG connection..."
        try {
            [void](Connect-PrtgServer -Server $Hostname -IgnoreSSL -Credential (New-Credential -Username $Username -Password $Password) -Force -ErrorAction Stop)
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Validation complete!"
        }
        catch [System.Net.WebException] { 
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] Could not resolve server with name: $Hostname" -ThrowException -Exception $_
        }
        catch [System.UriFormatException] {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] Invalid hostname: This may be caused by whitespace or the server name is too long" -ThrowException -Exception $_
        }
        catch [System.Net.Http.HttpRequestException] {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] PRTG username and/or password are incorrect. Please try again." -ThrowException -Exception $_
        }
        catch [System.Net.Sockets.SocketException] {
            Write-LogCritical "[$( $MyInvocation.MyCommand )] Unable to connect to $Hostname. Resource is not reachable. Do you have network access to the server?" -ThrowException -Exception $_
        }
        catch {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] $( $_.Exception.Message )" -ThrowException -Exception $_
        }
    }
}
function Convert-EC2SCSITargetIdToDeviceName {
    param(
        [Parameter(Mandatory = $true)]    
        [int]
        $SCSITargetId
    )
    if ($SCSITargetId -eq 0) { return "sda1" }
    $deviceName = "xvd"
    if ($SCSITargetId -gt 25) { $deviceName += [char](0x60 + [int]($SCSITargetId / 26)) }
    $deviceName += [char](0x61 + $SCSITargetId % 26)
    return $deviceName
}
function ConvertTo-FormattedXmlString {
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [xml]
        $XmlContent,
        [Boolean]
        $EnableIndentation = $true,
        [Int]
        $IndentChars = 4,
        [Boolean]
        $NewLineOnAttributes = $true
    )
    process {
        Using-Object($memoryStream = New-Object System.IO.MemoryStream) {
            $settings = New-Object System.Xml.XmlWriterSettings
            $settings.Indent = $EnableIndentation
            $settings.IndentChars = " " * $IndentChars
            $settings.NewLineOnAttributes = $NewLineOnAttributes
            Using-Object($writer = [System.Xml.XmlWriter]::Create($memoryStream, $settings)) {
                $XmlContent.Save($writer)
                $writer.Flush()
            }
            $memoryStream.Position = 0
            Using-Object($formattedXml = New-Object System.IO.StreamReader($memoryStream)) {
                return $formattedXml.ReadToEnd()
            }
        }
    }
}
function Copy-ItemWithRetry {
    param (
        [string]$Path,
        [string]$Destination,
        [int]$WaitTimeInSeconds = 3,
        [int]$MaxRetryCount = 20
    )
    $retryCount = 0
    $success = $false
    do {
        $retryCount++
        try {
            Copy-Item -Path $Path -Destination $Destination -Force -ErrorAction Stop | Out-Null
            $success = $true
            break
        }
        catch {
            Write-LogWarrning -Message "[$( $MyInvocation.MyCommand )] Error occurred: $_"
            Write-LogWarrning -Message "[$( $MyInvocation.MyCommand )] Retrying in $WaitTimeInSeconds seconds..."
            Start-Sleep -Seconds $WaitTimeInSeconds
        }
    } while ($retryCount -le $MaxRetryCount)
    if (!$success) {
        Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] Failed to copy $Path to $Destination after $retryCount attempts." -ThrowException 
    }
}
function Format-EnvironmentType {
    param (
        [Parameter(Mandatory = $true)]
        [String]$Environment
    )
    $result = [PSCustomObject]@{
        LongName   = ""
        TagValue   = ""
        MediumName = ""
        ShortName  = ""
    }
    switch ($Environment) {
        "customer_production" { 
            $result.LongName = "customer_production"
            $result.MediumName = "prod"
            $result.ShortName = "p"
        }
        "customer_non_production" { 
            $result.LongName = "customer_non_production"
            $result.MediumName = "nonprod"
            $result.ShortName = "n"
        }
        "internal" { 
            $result.LongName = "internal"
            $result.MediumName = "int"
            $result.ShortName = "i"
        }
    }
    $result.TagValue = $result.LongName
    return $result
}
function Get-AntivirusName {
    if ($IsWindows) {
        $isCrowdStrikeInstalled = Test-IsAppInstalled -SearchTerm "*CrowdStrike*Win*Sensor*"
        if ($isCrowdStrikeInstalled) {
            return 'Crowdstrike';
        }
        $isTrendInstalled = Test-IsAppInstalled -SearchTerm "*Trend*"
        if ($isTrendInstalled) {
            return 'Trend';
        }
    }
    else {
        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] This platform is not supported yet. Skipping."
    }
}
function Get-AWSCurrentUser {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]$Region
    )
    process {
        try {
            $stsCaller = Get-STSCallerIdentity -Region $Region
            if ($stsCaller.Arn -like "arn:aws:iam::*:user/*") {
                return $stsCaller.arn.Split("user/")[1].ToString()
            }
            if ($stsCaller.Arn -like "arn:aws:sts::*:assumed-role/*") {
                return $stsCaller.Arn.Split("assumed-role/")[1].Split("/")[-1].ToString()
            }
            return $stsCaller.Arn.ToString()
        }
        catch {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] $( $_.Exception.Message )" -ThrowException -Exception $_
        }
    }
}
function Get-AWSManagementSecurityGroups {
    [cmdletbinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]$VPCId,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]$Region
    )
    process {
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Getting the AWS management security groups..."
        try {
            $suffixes = @("-mgmt-services", "-mgmt-2-sg")
            $ids = @()
            $suffixes | ForEach-Object {
                $ids += (Get-EC2SecurityGroup -Region $Region -Filter @{ Name = "group-name"; Values = $Region + $_ }, @{ Name = "vpc-id"; Values = $VPCId }).GroupId
            }
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Groups found: $ids"
            return $ids
        }
        catch {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] $( $_.Exception.Message )" -ThrowException -Exception $_
        }
    }
}
function Get-AWSRoute53Record {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $Name,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        [ValidateSet("A", "CNAME", IgnoreCase = $false)]
        $Type,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $HostedZoneId,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $Region
    )
    process {
        try {
            $hostedZoneName = (Get-R53HostedZone -Id $HostedZoneId -Region $Region).HostedZone.Name.Trim(".")
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Searching for '$Type' record with name '$Name' in hosted zone '$hostedZoneName' ($HostedZoneId)..."
            $getResourceRecordSetParams = @{
                Id       = $HostedZoneId
                MaxItems = 300
            }
            $recordSets = @()
            $isTruncated = $true
            while ($isTruncated) {
                $batch = Get-R53ResourceRecordSet @getResourceRecordSetParams
                $recordSets += $batch.ResourceRecordSets
                if ($batch.IsTruncated) {
                    $getResourceRecordSetParams["StartRecordName"] = $batch.NextRecordName
                }
                else {
                    $isTruncated = $false
                }
            }
            foreach ($recordSet in $recordSets) {
                $recordName = ($recordSet.Name -split ".$hostedZoneName.")[0].ToLower()
                if ($recordName -eq $Name.ToLower() -and $recordSet.Type -eq $Type) {
                    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Record found!"
                    return $recordSet
                }
            }
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Record not found!"
        }
        catch {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] $( $_.Exception.Message )" -ThrowException -Exception $_
        }
    }
}
function Get-BrowserPlatformPath {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [PSObject]
        $TomcatWebAppPath
    )
    return [DTXBrowser]::new($TomcatWebAppPath).GetBrowserPlatformPath()
}
function Get-BrowserPlatformPropertiesFilePath {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [PSObject]
        $TomcatWebAppPath
    )
    return [DTXBrowser]::new($TomcatWebAppPath).GetBrowserPlatformPropertiesFilePath()
}
function Get-BrowserPlatformGifcachePath {
    return [DTXBrowser]::new((Get-TomcatWebAppPath)).GetBrowserPlatformGifCachePath()
}
function Get-BrowserPlatformGifcacheToolkitPath {
    return [DTXBrowser]::new((Get-TomcatWebAppPath)).GetBrowserPlatformGifCacheToolkitPath()
}
function Get-BrowserSysMonitor {
    [CmdletBinding()]
    param()
    function Get-BrowserPropertiesHashTable{
        param(
            [Parameter(Mandatory = $true)]
            [string]
            $PropertiesFilePath
        )
        if (!(Test-Path $PropertiesFilePath)){
          throw (New-Object DTXPSException("[$( $MyInvocation.MyCommand )] browser properties file path doesnt exist"))
        }
      return ConvertFrom-StringData (Get-Content $PropertiesFilePath -raw)
    }
    function Get-ApiCallHeaders{
       param(
            [Parameter(Mandatory = $true)]
            [hashtable]
            $PropertiesData,
            [string]
            $PropertyKey
        )
        $headers= @{}
        if ($PropertiesData.Contains($PropertyKey)){
            $ok_ip = $PropertiesData.$PropertyKey.split(",")[0] 
            $headers.Add("X-Forwarded-For", $ok_ip)
        }
        else{
        }
        return $headers
    }
    function Invoke-Api {
       param(
            [Parameter(Mandatory = $true)]
            $headers,
            [string]
            $APIEndpoint
        )
        try{
            $response = Invoke-WebRequest -Uri $APIEndpoint  -TimeoutSec 15 -SkipCertificateCheck -Headers $headers
            if ($response -ne $null)
            {
              if (Test-Json -Json $response.Content -ErrorAction SilentlyContinue) {
                return $response.Content | ConvertFrom-Json -AsHashtable
              }
              else{
                Write-LogWarning "[$( $MyInvocation.MyCommand )] Failed to detect data from Web Response as JSON, returning empty dict"
                Write-LogWarning "[$( $MyInvocation.MyCommand )] Data: $($response.Content)"
                return New-Object System.Collections.Hashtable
              }
            }
            else{
              Write-LogWarning "[$( $MyInvocation.MyCommand )] Failed to see response as non null, returning empty dict"
              return New-Object System.Collections.Hashtable
            }
        }
        catch{
          Write-LogWarning "[$( $MyInvocation.MyCommand )] Failed to execute web request to SystemMonitor.do: $($_.Exception.Message | Out-String)"
          Write-LogWarning "[$( $MyInvocation.MyCommand )] returning empty dict"
          return New-Object System.Collections.Hashtable
        }
    }
    if(!(Test-IsBrowserSystem)){
      Write-LogCritical -ThrowException "[$( $MyInvocation.MyCommand )] Is not a browser system but calling health check API"
    }
    $defaults = Get-Defaults
    $monitoringIpsKey = $defaults.Browser.Properties.MonitoringIpsKey
    $ApiEndpoint = $defaults.Browser.HealthCheck.APIEndpoint
    $browserProperties = [DTXBrowser]::new((Get-TomcatWebAppPath)).GetBrowserPropertiesData()
    [DTXBrowserSysMonitor]::Perform($monitoringIpsKey, $ApiEndpoint, $browserProperties)
}
function Get-ComputerName {
    [cmdletbinding()]
    param (
        [Parameter(Mandatory = $true)]
        [String]$CustomerName,
        [Parameter(Mandatory = $true)]
        [String]$Environment,
        [Parameter(Mandatory = $true)]
        [String]$RandomString
    )
    process {
        if ($CustomerName.Length -ge 8) {
            $CustomerName = $CustomerName.Substring(0, 7)
        }
        $CustomerName = $CustomerName -replace '[^a-zA-Z0-9]', ''
        $name = "$CustomerName-$Environment-$RandomString".ToLower()
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] ComputerName: $name"
        if ($name.Length -gt 15) {
            Write-LogCritical -ThrowException -Message "[$( $MyInvocation.MyCommand )] ComputerName is too long. Max length is 15 characters."
        }
        return $name
    }
}
function Get-DBInfoFilePath {
    return Join-Path (Get-LocalDotmaticsPath) "db-info.txt"
}
function Get-Defaults {
    function Get-FileContent {
        param (
            [string]$FilePath
        )
        if (Test-Path -Path $FilePath) {
            return Get-Content -Path $FilePath -Raw
        }
        else {
            throw [System.IO.FileNotFoundException] "File $FilePath not found."
        }
    }
    $jsonContent = ""
    if ($env:DTX_DEFAULTS_JSON_FILE_PATH) {
        $jsonContent = Get-FileContent -FilePath $env:DTX_DEFAULTS_JSON_FILE_PATH
    }
    else {
        $externalFilePath = Get-ExternalFilePath -FileName "defaults.json"
        $jsonContent = Get-FileContent -FilePath $externalFilePath
    }
    $jsonContentWithoutComments = [DTXJsonUtils]::RemoveJsonComments($jsonContent)
    return $jsonContentWithoutComments | ConvertFrom-Json -Depth 100 -AsHashtable
}
function Get-DiskDriveLetter {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $DiskPath
    )
    if ($IsWindows) {
        $diskNumber = (Get-Disk -Path $DiskPath).Number
        $driveLetter = $null
        if ($diskNumber -eq 0) {
            $driveLetter = "C"
        }
        else {
            try {
                $driveLetter = (Get-Partition -DiskNumber $diskNumber).DriveLetter
                if (-not $driveLetter) {
                    $driveLetter = ((Get-Partition -DiskId $DiskPath).AccessPaths).Split(",")[0]
                } 
                if ($driveLetter.Count -gt 1) {
                    $driveLetter = $driveLetter | Where-Object { $_ -match "[A-Z]" }
                }
            }
            catch {
                Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Cannot get drive letter for disk number '$diskNumber'. Skipping..."
                return $null
            }
        }
        return $driveLetter
    }
    else {
        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] This function is only supported on Windows. Skipping..."
    }
}
function Get-DiskInformation {
    $returnObj = New-Object Collections.Generic.List[PSObject]
    $sysInfo = Get-SystemInfo
    if (-not $IsWindows) {
        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Cannot get disk information on a non-Windows machine for now. Skipping..."
        return , $returnObj
    }
    if ($IsWindows -and $sysInfo.Windows.VersionAsYear -le 2012) {
        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Cannot get disk information on Windows 2012 or older. Skipping..."
        return , $returnObj
    }
    foreach ($disk in Get-Disk) {
        $diskNumber = $disk.Number
        $deviceName = $disk.FriendlyName
        $partitionsCount = $disk.NumberOfPartitions
        $driveLetter = Get-DiskDriveLetter -DiskPath $disk.Path
        $ebsVolumeId = Get-EBSVolumeId -DiskPath $disk.Path
        $virtualDevice = $null
        $blockDeviceName = $null
        $volumeName = (Get-PSDrive | Where-Object { $_.Name -in @($driveLetter) }).Description | Where-Object { $_ -notin @("", $null) }
        $blockDeviceMappings = (Get-EC2InstanceMetadata -Category "BlockDeviceMapping") | Where-Object { $_.Key -ne "ami" }
        if ($disk.Path -like "*PROD_PVDISK*") {
            $blockDeviceName = Convert-EC2SCSITargetIdToDeviceName((Get-CimInstance -Class Win32_Diskdrive | Where-Object { $_.DeviceID -eq ("\\.\PHYSICALDRIVE" + $diskNumber) }).SCSITargetId)
            $blockDeviceName = "/dev/" + $blockDeviceName
            $virtualDevice = ($blockDeviceMappings | Where-Object { $_.Value -eq $blockDeviceName }).Key | Select-Object -First 1
        }
        if ($disk.Path -like "*PROD_AMAZON_EC2_NVME*") {
            $blockDeviceName = $blockDeviceMappings.ephemeral((Get-CimInstance -Class Win32_Diskdrive | Where-Object { $_.DeviceID -eq ("\\.\PHYSICALDRIVE" + $diskNumber ) }).SCSIPort - 2)
            $virtualDevice = ($blockDeviceMappings | Where-Object { $_.Value -eq $blockDeviceName }).Key | Select-Object -First 1
        }
        $diskToAdd = New-Object PSObject -Property @{
            Disk          = $disk | Select-Object -ExcludeProperty Cim*
            Partitions    = $partitionsCount
            DriveLetter   = $driveLetter ?? "N/A";
            EbsVolumeId   = $ebsVolumeId ?? "N/A";
            Device        = $blockDeviceName ?? "N/A";
            VirtualDevice = $virtualDevice ?? "N/A";
            VolumeName    = $volumeName ?? "N/A";
            DeviceName    = $deviceName ?? "N/A";
        }
        $returnObj.Add($diskToAdd)
    }
    $sysVolumes = Get-Volume
    foreach ($volume in $sysVolumes) {
        $matchedDisk = $returnObj | Where-Object { $_.DriveLetter -eq $volume.DriveLetter }
        if ($matchedDisk) {
            $matchedDisk | Add-Member -MemberType NoteProperty -Name 'VolumeSizeGB' -Value ([math]::Round($volume.Size / 1GB, 2))
            $matchedDisk | Add-Member -MemberType NoteProperty -Name 'VolumeSpaceLeftGB' -Value ([math]::Round($volume.SizeRemaining / 1GB, 2))
            $matchedDisk | Add-Member -MemberType NoteProperty -Name 'VolumePercentFree' -Value ([math]::round(($volume.SizeRemaining / $volume.Size) * 100, 2))
        }
    }
    return $returnObj
}
function Get-EBSVolumeId {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $DiskPath
    )
    if ($IsWindows) {
        $serialNumber = (Get-Disk -Path $DiskPath).SerialNumber
        $ebsVolumeId = $null
        if ($serialNumber -like 'vol*') {
            $ebsVolumeId = $serialNumber.Substring(0, 20).Replace("vol", "vol-")
        }
        elseif ($serialNumber -like 'aws*') {
            $ebsVolumeId = $serialNumber.Substring(0, 20).Replace("AWS", "AWS-")
        }
        else {
            Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Could not find EBS volume ID for disk '$DiskPath'."
        }
        return $ebsVolumeId
    }
    else {
        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] This function is only supported on Windows. Skipping..."
    }
}
function Get-EC2Tags {
  param (
    [string] $InstanceId
  )
  Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Getting Tags for EC2"
  $instanceTags = Get-EC2Tag -Filter @{ Name = "resource-id"; Values = $InstanceId }
  Assert-True -Condition ($instanceTags.Length -gt 0) -message "Instance Tags was length 0"
  Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Total tag count returned: $($instanceTags.Length)" 
  return $instanceTags
}
function Get-TagValue {
  param (
    [string] $Key,
    [array]  $Tags
  )
  Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Finding tag value for key: $key"
  foreach ($tag in $Tags) {
    if ($tag.Key -eq $Key) {
      return $tag.Value
    }
  }
  Write-LogWarning "[$( $MyInvocation.MyCommand )] Tag key is NOT found in list of tags" 
  throw (New-Object DTXPSException("[$( $MyInvocation.MyCommand )] Tag key: $Key not found"))
}
function Get-ExternalFilePath {
    param(
        [string] $FileName
    )
    return (New-ExternalFilesProvider).GetFilePath($FileName)
}
function Get-FileFromS3 {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $BucketName,
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $BucketKey,
        [Parameter(Mandatory = $false)]
        [string]
        $DirectoryPath,
        [switch]
        $PreserveFileName
    )
    return [DTXS3Utils]::GetFile($BucketName, $BucketKey, $DirectoryPath, $PreserveFileName)
}
function Get-GlowrootInfo {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [PSObject]
        $TomcatJavaOptions
    )
    $res = [ordered]@{
        IsGlowrootInJavaOptions     = "UNKNOWN"
        IsGlowrootLaunched     = "UNKNOWN"
        TextInfo = "UNKNOWN"
    }
    if (-not $TomcatJavaOptions ) {
        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Unable to determine Glowroot info since Tomcat Options are not available"
        return $res
    }
    $glowrootMessage1 = ""
    $glowrootMessage2 = ""
    if (([string]::Join('', $TomcatJavaOptions).Contains("glowroot"))) {
        $res.IsGlowrootInJavaOptions = $true
        $glowrootMessage1 = "Glowroot is present in tomcat java options"
    }
    else {
        $res.IsGlowrootInJavaOptions = $false
        $glowrootMessage1 = "Glowroot is absent in tomcat java options"
    }
    try {
        $webResponse = Invoke-WebRequest -uri "http://localhost:4000" -UseBasicParsing
        $s = $webResponse.content
    }
    catch {
        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Glowroot URL localhost:4000 invoke failed. $( $_.Exception.Message ) "
        $s = ""
    }
    if ([string]::IsNullOrEmpty($s)) {
        $res.IsGlowrootLaunched = $false
        $glowrootMessage2 = "Glowroot is not launched"
    }
    else {
        $res.IsGlowrootLaunched = $true
        $glowrootMessage2 = "Glowroot is launched"
    }
    $res.TextInfo = "$glowrootMessage1. $glowrootMessage2"
    return $res
}
function Get-InfoFromDatabase {
    param(
        [psobject]
        [ValidateNotNullOrEmpty()]
        $ReturnObjParam)
    function Write-DBInfoFile {
        $sid = Get-OraclePluggableDbSid
        if ($sid) {
            $dbInfoFile = Get-DBInfoFilePath
            $orclSqlFilePath = Get-ExternalFilePath -FileName "sql-orcl.sql"
            $orclpdbSqlFilePath = Get-ExternalFilePath -FileName "sql-orclpdb.sql"
            "{oracle_version}" > $dbinfoFile
            try {
                & 'sqlplus.exe' '/ as sysdba' "@$orclSqlFilePath" >> $dbinfoFile
                & 'sqlplus.exe' "/@$sid as sysdba" "@$orclpdbSqlFilePath" >> $dbinfoFile
            }
            catch {
                Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] sqlplus is not found. Skipping."
            }
            "{end}" >> $dbinfoFile
        }
    }
    function Get-OracleVersion {
        param([string]$OracleStartOutput)
        $i = $OracleStartOutput.IndexOf("Connected to:")
        if ($i -eq -1) {
                return $null;
        }
        $res = $OracleStartOutput.Substring($i + 14)
        return $res    
    }
    function Read-AllFromDBInfoFile {
        $res = @{
            OracleVersion         = "UNKNOWN"
            Memory                = [ordered]@{
                MemoryMaxTarget    = "UNKNOWN"
                MemoryTarget       = "UNKNOWN"
                PgaAggregateLimit  = "UNKNOWN"
                PgaAggregateTarget = "UNKNOWN"
                SgaMaxSize         = "UNKNOWN"
                SgaTarget          = "UNKNOWN"
                TextInfo           = "UNKNOWN"
            }
            ProcessesSessions     = "UNKNOWN"
            Processes             = "UNKNOWN"
            Sessions              = "UNKNOWN"
            OpenCursors           = "UNKNOWN"
            ProjectsNumber        = "UNKNOWN"
            DatasourceCount       = [ordered]@{
                DynamicDatasourceCount    = "UNKNOWN"
                BrowserManagedTablesCount = "UNKNOWN"
                BrowserManagedViewsCount  = "UNKNOWN"
                DatasourceCountInfo       = "UNKNOWN"
            }
            MaterialisedViewCount = "UNKNOWN"
            PivotTableCount       = "UNKNOWN"
            Version               = [ordered]@{
                BrowserVersion     = "UNKNOWN"
                BioregisterVersion = "UNKNOWN"
                PinpointVersion    = "UNKNOWN"
                AllVersionsText    = "UNKNOWN"
            }
        }
        $allProperties = Read-AllPropertiesFromDBInfoFile
        if ($null -eq $allProperties) {
            return $null
        }
        $oracleStartOutput = $allProperties["{oracle_version}"]
        $res.OracleVersion = Get-OracleVersion $oracleStartOutput
        $res.Memory.TextInfo = $allProperties["{oracle_memory}"]
        $res.Memory.MemoryMaxTarget = $allProperties["{memory_max_target}"]
        $res.Memory.MemoryTarget = $allProperties["{memory_target}"]
        $res.Memory.PgaAggregateLimit = $allProperties["{pga_aggregate_limit}"]
        $res.Memory.PgaAggregateTarget = $allProperties["{pga_aggregate_target}"]
        $res.Memory.SgaMaxSize = $allProperties["{sga_max_size}"]
        $res.Memory.SgaTarget = $allProperties["{sga_target}"]
        $res.ProcessesSessions = $allProperties["{processes_sessions}"]
        $res.Processes = $allProperties["{processes}"]
        $res.Sessions = $allProperties["{sessions}"]
        $res.OpenCursors = $allProperties["{open_cursors}"]
        $res.ProjectsNumber = $allProperties["{projects_number}"]
        $res.DatasourceCount.DatasourceCountInfo = $allProperties["{ds_counts}"]
        $res.DatasourceCount.DynamicDatasourceCount = $allProperties["{cnt_dynamic_datasources}"]
        $res.DatasourceCount.BrowserManagedTablesCount = $allProperties["{cnt_br_managed_tables}"]
        $res.DatasourceCount.BrowserManagedViewsCount = $allProperties["{cnt_br_managed_views}"]
        $res.MaterialisedViewCount = $allProperties["{mv_count}"]
        $res.PivotTableCount = $allProperties["{pivot_count}"]
        $res.Version.AllVersionsText = $allProperties["{version}"]
        $res.Version.BrowserVersion = $allProperties["{browser_version_number}"]
        $res.Version.BioregisterVersion = $allProperties["{bioregister_version}"]
        $res.Version.PinpointVersion = $allProperties["{pinpoint_version_number}"]
        return $res
    }
    if ($IsWindows) {
        Write-DBInfoFile
        $res = Read-AllFromDBInfoFile
        return $res
    }
    else {
        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] This platform is not supported yet. Skipping."
    }
}
function Get-InfoFromOracleLogs {
    function Write-FrequencyInfoFiles {
        function OutputResult {
            param (
                [array]$Keys, [hashtable]$Freq, [string]$OutputFile
            )
            for ($i = 0; $i -lt $Keys.count; $i++) {
                $str = $Keys[$i] + " = " + $Freq[$Keys[$i]]
                Add-Content -path $OutputFile -Value $str
            }
        }
        function Get-OracleRuntimeSid {
            $dbInfoFile = Join-Path $(Get-LocalDotmaticsPath) "db-sid.txt"
            $sqlFilePath = Get-ExternalFilePath -FileName "sql-get-sid.sql"
            try {
                & 'sqlplus.exe' '/ as sysdba' "@$sqlFilePath" > $dbinfoFile
            }
            catch {
                Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] sqlplus is not found. Skipping."
            }
            return Read-PropertyFromDBInfoFile "{sid}" $dbInfoFile
        }
        function Get-OracleAlertLogFileFolder {
            $dbInfoFile = Join-Path $(Get-LocalDotmaticsPath) "db-alert-log-path.txt"
            $sqlFilePath = Get-ExternalFilePath -FileName "sql-get-alert-log-path.sql"
            try {
                & 'sqlplus.exe' '/ as sysdba' "@$sqlFilePath" > $dbinfoFile
            }
            catch {
                Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] sqlplus is not found. Skipping."
            }
            return Read-PropertyFromDBInfoFile "{alert_log_file_path}" $dbInfoFile
        }
        function CalculateFrequency {
            param (
                [string]
                $SearchFor, 
                [string]
                $OutputFile
            )
            if (Test-Path $OutputFile) {
                Clear-Content $OutputFile -Force
            }
            $freq = @{ }
            [string[]]$arrayFromFile = Get-Content -Path $oracleAlertLogPath
            $i = 0
            foreach ($line in $arrayFromFile) {
                if ($line -clike "*" + $SearchFor + "*" ) {
                    $j = $i - 1;
                    for ($j = $i - 1; ; $j--) {
                        if ($arrayFromFile[$j] -match '^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d' ) {
                            $dateRegexp = 'yyyy-MM-ddTHH:mm:ss'
                            $dateSubstrFrom = 0
                            $dateSubstrTo = 19
                            break;
                        }
                        if ($arrayFromFile[$j] -match '^(Mon|Tue|Wed|Thu|Fri|Sat|Sun) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d\d \d\d:\d\d:\d\d \d\d\d\d' ) {
                            $dateRegexp = 'MMM dd HH:mm:ss yyyy'
                            $dateSubstrFrom = 4
                            $dateSubstrTo = 20
                            break;
                        }
                    }
                    $timeStr = $arrayFromFile[$j].Substring($dateSubstrFrom, $dateSubstrTo)
                    $d = [datetime]::parseexact($timeStr, $dateRegexp, $null)
                    $dateKey = $d.ToString("yyyy-MM-dd")
                    if ($freq.ContainsKey($dateKey)) {
                        $freq[$dateKey]++
                    }
                    else {
                        $freq[$dateKey] = 1
                    }
                }
                $i++
            }
            $Keys = ($Freq.Keys | Sort-Object)
            OutputResult $Keys $Freq $OutputFile    
        }
        $folder = Get-OracleAlertLogFileFolder
        if (-not ([System.IO.File]::Exists($folder)) ) {
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Oracle Alert log file folder not found"
            return $null;
        }
        $sid = Get-OracleRuntimeSid
        $oracleAlertLogPath = Join-Path $($folder) "alert_$sid.log"
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Oracle Alert log file path: $($oracleAlertLogPath)"
        $searchFor = "WARNING: Heavy swapping observed on system in last 5 mins."
        $OutputFile = Get-OracleLogsheavySwappingFile
        CalculateFrequency $searchFor $OutputFile
        $searchFor1 = "Checkpoint not complete"
        $OutputFile1 = Get-OracleLogsCheckpointNotCompleteFile
        CalculateFrequency $searchFor1 $OutputFile1
        $searchFor2 = "ORA-04036: PGA memory used by the instance exceeds PGA_AGGREGATE_LIMIT"
        $OutputFile2 = Get-OracleLogsPgaLimitFile
        CalculateFrequency $searchFor2 $OutputFile2
    }
    function Read-DatabaseAlertLogsFile {
        $res = [ordered]@{
            LatestLogsCheckpointNotComplete = "UNKNOWN"
            LatestLogsHeavySwapping         = "UNKNOWN"
            LatestLogsPgaLimit              = "UNKNOWN"
        }
        try {
            $res.LatestLogsCheckpointNotComplete = Get-Content -Path $(Get-OracleLogsCheckpointNotCompleteFile) -tail 10
        }
        catch {
            Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] File Frequency-checkpoint-not-complete.txt not found. Skipping."
        }
        try {
            $res.LatestLogsHeavySwapping = Get-Content -Path $(Get-OracleLogsHeavySwappingFile) -tail 10
        }
        catch {
            Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] File Frequency-warning-heavy-swapping.txt not found. Skipping."
        }
        try {
            $res.LatestLogsPgaLimit = Get-Content -Path $(Get-OracleLogsPgaLimitFile) -tail 10
        }
        catch {
            Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] File Frequency-PGA_AGGREGATE_LIMIT.txt not found. Skipping."
        }
        return $res
    }
    function Get-OracleLogsHeavySwappingFile {
        return Join-Path $(Get-LocalDotmaticsPath) "Frequency-warning-heavy-swapping.txt"
    }
    function Get-OracleLogsCheckpointNotCompleteFile {
        return Join-Path $(Get-LocalDotmaticsPath) "Frequency-checkpoint-not-complete.txt"
    }
    function Get-OracleLogsPgaLimitFile {
        return Join-Path $(Get-LocalDotmaticsPath) "Frequency-PGA_AGGREGATE_LIMIT.txt"
    }
    if ($IsWindows) {
        Write-FrequencyInfoFiles
        return Read-DatabaseAlertLogsFile
    }
    else {
        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] This platform is not supported yet. Skipping."
    }
}
function Get-InstalledApps {
    if ($IsWindows) {
        $WindowsUninstallRegKeys = @(
            "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall",
            "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall"
        )
        return Get-ChildItem $WindowsUninstallRegKeys | Get-ItemProperty | Where-Object { $_.PSObject.Properties.Name -contains "DisplayName" }
    }
    else {
        Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] This platform is not supported yet." -ThrowException 
    }
}
function Get-InstanceMetadata {
    param (
        [string] $BaseUrl = "http://169.254.169.254/latest",
        [string] $MetadataBaseUrl = "$BaseUrl/meta-data",
        [string] $InstanceIdentityBaseUrl = "$BaseUrl/dynamic/instance-identity/document"
    )
    $instanceId = (Invoke-RestMethod -Uri "$MetadataBaseUrl/instance-id").ToString()
    $instanceType = (Invoke-RestMethod -Uri "$MetadataBaseUrl/instance-type").ToString()
    $region = (Invoke-RestMethod -Uri $InstanceIdentityBaseUrl).region.ToString()
    return [PSCustomObject]@{
        InstanceId   = $instanceId
        InstanceType = $instanceType
        Region       = $region
    }
}
function Get-InstanceName {
    [cmdletbinding()]
    param (
        [Parameter(Mandatory = $true)]
        [String]$CustomerName,
        [Parameter(Mandatory = $true)]
        [String]$Region,
        [Parameter(Mandatory = $true)]
        [String]$Environment,
        [Parameter(Mandatory = $true)]
        [String]$RandomString
    )
    process {
        $name = "$Region-$CustomerName-$Environment-$RandomString".ToLower()
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Instance name: $name"
        return $name
    }
}
function Get-InstanceUrl {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        $InstanceTags
    )
    $urlPattern = "https://[\w\.\-]+.dotmatics.net"
    try {
        $url = Get-TagValue -Key 'Url' -Tags $InstanceTags -ErrorAction "SilentlyContinue"
    } catch {
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] URL was not identified using Url key in instance tags."
    }
    if ($url -match $urlPattern) {
        return $url
    }
    foreach ($value in $InstanceTags.Values) {
        if ($value -match $urlPattern) {
            return $value
        }
    }
    return "UNKNOWN"
}
function Get-JChemCartridgeHomePathUsingProcess {
    $thePath = $null
    $process = Get-JChemCartridgeProcess
    if (-not $process) {
        return $null
    }
    $thePath = [System.IO.Path]::GetDirectoryName($process.Path)
    if ($null -eq $thePath) {
        return $null
    }
    if (-not (Test-Path -Path $thePath)) {
        return $null
    }
    return $thePath
}
function Get-JChemCartridgeHomePathUsingServices {
    if ($IsWindows) {
        $service = Get-JChemCartridgeService
        if (!$service) {
            return $null
        }
        return [System.IO.Path]::GetDirectoryName((Join-Path -Path ($service.BinaryPathName -Split "cartridge")[0] -ChildPath ("cartridge" + [System.IO.Path]::DirectorySeparatorChar)))
    }
    return $null
}
function Get-JChemCartridgeHomePath {
    $thePath = $null
    if ($IsWindows) {
        $thePath = Get-JChemCartridgeHomePathUsingServices
        if ($thePath) {
            return $thePath
        }
    }
    $thePath = Get-JChemCartridgeHomePathUsingProcess
    if ($thePath) {
        return $thePath
    }
    Write-LogCritical -ThrowException "[$( $MyInvocation.MyCommand )] Unable to determine JChem Cartridge Service's home path."
}
function Get-JChemCartridgeProcess {
    $process = Get-Process | Where-Object { ($_.ProcessName -like "*prunsrv-*") -and ($_.CommandLine -like "*CartridgeService*") }
    if ($null -eq $process) {
        return $null
    }
    if ($proces -is [System.Array]) {
        Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] Multiple JChem Cartridge processes found. This is not expected." -ThrowException 
    }
    return $process
}
function Get-JChemCartridgeService {
    $service = Get-Service | Where-Object { ($_.Name -like "*jchem*cartridge*") -and ($_.BinaryPathName -like "*prunsrv*") }
    if (!$service) {
        $process = Get-JChemCartridgeProcess
        $service = Get-CimInstance -Class Win32_Service -Filter ("ProcessId LIKE '" + $process.Id + "'")
        $service = Get-Service $service.Name
    }
    if (!$service) {
        return $null
    }
    if ($service.Length -gt 1) {
        Write-LogCritical -ThrowException -Message "[$( $MyInvocation.MyCommand )] Unable to determine the JChem Service. Multiple services match the search criteria. Do we have two or more JChem.exe processes running on this machine?"
    }
    return $service
}
function Get-JChemCartridgeVersion {
    $homePath = Get-JChemCartridgeHomePath
    $versionPropsFile = Join-Path -Path (Split-Path $homePath -Parent) -ChildPath "version.properties"
    if (!(Test-Path $versionPropsFile)) {
        return $null
    }
    $fileContents = Get-Content $versionPropsFile -Raw | ConvertFrom-StringData
    if ($fileContents.ContainsKey("version")) {
        return $fileContents.Version
    }
    return $null
}
function Get-JChemMetadata {
    $returnObj = @{
        Cartridge = @{
            Process  = $null
            Service  = $null
            HomePath = $null
            Version  = $null
        }
    }
    try {
        $returnObj.Cartridge.Process = Get-JChemCartridgeProcess
    }
    catch {
        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Unable to find the JChem Cartridge process."
    }
    try {
        $returnObj.Cartridge.Service = Get-JChemCartridgeService
    }
    catch {
        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Unable to find the JChem Cartridge service."
    }
    try {
        $returnObj.Cartridge.HomePath = Get-JChemCartridgeHomePath
    }
    catch {
        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Unable to find the JChem Cartridge home path."
    }
    try {
        $returnObj.Cartridge.Version = Get-JChemCartridgeVersion
    }
    catch {
        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Unable to determine the JChem Cartridge version."
    }
    return $returnObj
}
function Get-LicenseNumber {
    [CmdletBinding()]
    param(
        [string]
        $WebAppPath 
    )
    try {
        if ([string]::IsNullOrEmpty($WebAppPath)) {
            $WebAppPath = Get-TomcatWebAppPath
        } 
    } catch {
        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Unable to determine WebApp folder path"
    }
    if (-not $WebAppPath) {
        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Unable to determine license number since WebApp folder path is uknown"
        return "UNKNOWN"
    }
    $defaults = Get-Defaults
    $pathToLicenseFile = Join-Path $WebAppPath $defaults.Browser.LicenseFilePath
    if (-not (Test-Path $pathToLicenseFile)) {
        return "UNKNOWN"
    }
    $fileLineLicenseNumber = Get-Content $pathToLicenseFile |  where {$_ -like '*browser license seats:*'} | Select-Object -First 1
    if ($fileLineLicenseNumber -match '(\d{1,})') {
        $licenseCount = $Matches[1]
        return $licenseCount
    } else {
        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Unable to get license number."
        return "UNKNOWN"
    }
}
function Get-LocalDotmaticsPath {
    return [DTXPathProvider]::GetLocalDotmaticsPath()
}
function Get-LocalDotmaticsSharedPath {
    return [DTXPathProvider]::GetLocalDotmaticsSharedPath()
}
function Get-LocalStateFilePath {
    return [DTXPathProvider]::GetLocalStateFilePath()
}
function Get-LocalTempPath {
    return [DTXPathProvider]::GetLocalTempPath()
}
function Get-OpenEyeDefaults {
    param (
        [string] $Version
    )
    $defaults = Get-Defaults
    if (!$defaults) {
        Write-LogCritical -ThrowException "[$( $MyInvocation.MyCommand )] Unable to get the defaults.json file. Are you authenticated?"
    }
    $openEyeDefaults = $defaults.OpenEye."v$Version"
    if (!$openEyeDefaults) {
        Write-LogCritical -ThrowException "[$( $MyInvocation.MyCommand )] Unable to locate OpenEye version '$Version' in the defaults.json file."
    }
    return $openEyeDefaults
}
function Get-OpenEyeMetadata {
    $returnObj = @{
        UserFriendlyVersion   = $null
        Mol2NameChecksum      = $null
        Mol2NameBatchChecksum = $null
    }
    if (!$IsWindows -and $IsLinux) {
        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Only Windows and Linux operating systems are supported."
        return $returnObj
    }
    try {
        $gifcacheToolkitPath = Get-BrowserPlatformGifcacheToolkitPath
    }
    catch {
        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Unable to locate the Browser gifcache/toolkit directory."
        return $returnObj
    }
    try {
        if ($IsWindows) {
            $mol2NameFileName = "mol2name.exe"
            $mol2NameFilePath = Join-Path $gifcacheToolkitPath $mol2NameFileName
            $mol2NameBatchFileName = "mol2name_batch.exe"
            $mol2NameBatchFilePath = Join-Path $gifcacheToolkitPath $mol2NameBatchFileName
        }
        if ($IsLinux) {
            $mol2NameFileName = "mol2name"
            $mol2NameFilePath = Join-Path $gifcacheToolkitPath $mol2NameFileName
            $mol2NameBatchFileName = "mol2name_batch"
            $mol2NameBatchFilePath = Join-Path $gifcacheToolkitPath $mol2NameBatchFileName
        }
    }
    catch {
        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Unable to set the mol2name and mol2namebatch file paths."
        return $returnObj
    }
    try {
        if (Test-Path $mol2NameFilePath) {
            $returnObj.Mol2NameChecksum = (Get-FileHash -Path $mol2NameFilePath -Algorithm MD5).Hash
        }
        else {
            Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Unable to find the the OpenEye Mol2Name binary."
        }
    }
    catch {
        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Unable to find the the OpenEye Mol2Name checksum."
    }
    try {
        if (Test-Path $mol2NameBatchFilePath) {
            $returnObj.Mol2NameBatchChecksum = (Get-FileHash -Path $mol2NameBatchFilePath -Algorithm MD5).Hash
        }
        else {
            Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Unable to find the the OpenEye Mol2NameBatch binary."
        }
    }
    catch {
        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Unable to find the the OpenEye Mol2NameBatch checksum."
    }
    try {
        if ($returnObj.Mol2NameChecksum -or $returnObj.Mol2NameBatchChecksum) {
            $openEyeDefaults = (Get-Defaults).OpenEye
            foreach ($prop in $openEyeDefaults.Keys) {
                if ($prop -like "v20*") {
                    $versionNamespace = $prop
                    $selectedNamespace = $openEyeDefaults.$versionNamespace
                    $selectedFiles = $selectedNamespace.FileChecksums.Files
                    if (($returnObj.Mol2NameChecksum -eq $selectedFiles.$mol2NameFileName) -or ($returnObj.Mol2NameBatchChecksum -eq $selectedFiles.$mol2NameBatchFileName)) {
                        $returnObj.UserFriendlyVersion = $selectedNamespace.UserFriendlyVersion
                        break
                    }
                }
            }
        }
    }
    catch {
        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Unable to find the the OpenEye user friendly version"
    }
    return $returnObj
}
function Get-OracleConnectionDescription {
    [cmdletbinding()]
    param ()
    $output = (& 'lsnrctl' 'status')
    $services = @()
    foreach ($l in $output) {
        if ($l -match '\(DESCRIPTION=\(ADDRESS=\(PROTOCOL=TCP\)\(HOST=(.+)\)\(PORT=(.+)\)\)\)') {
            $connStr = $Matches[0]
            $dbhost = $Matches[1]
            Write-Debug "host is $dbhost"
            $dbport = $Matches[2]
            Write-Debug "port is $dbport"
        }
        if ($l -match 'Service \"(.+)\" has \d instance\(s\)') {
            $srv = $Matches[1]
            if ($srv -notlike "*XDB" -and $srv -notlike "*Proc" -and ($srv.length) -ne 32) {
                $services += $Matches[1]
            }
        }
    }
    write-debug "DB services $services"
    $dbService = $null
    if ($services.count -eq 1) {
        $dbService = $services[0]
    }
    elseif ($services.count -eq 2) {
        $filteredServices = $services | Where-Object { $_ -ne "orcl" }
        if ($filteredServices.count -eq 1) {
            $dbService = $filteredServices
        }
    }
    else {
        $dbService = $services[0]
    }
    Write-Debug "Service_name is $dbService"
    if ($connStr -match '\(DESCRIPTION=\(ADDRESS=\(PROTOCOL=TCP\)\(HOST=(.+)\)\(PORT=(.+)\)\)\)') {
        $connStr = "(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=$dbhost)(PORT=$dbport))(CONNECT_DATA=(SERVICE_NAME=$dbService)))"
    }
    return $connStr
}
function Get-OracleDatabaseBasePath {
    if ($IsWindows) {
        $dbInfoFile = Join-Path $(Get-LocalDotmaticsPath) "db-oraclebase-path.txt"
        $sqlFilePath = Get-ExternalFilePath -FileName "sql-get-oraclebase-path.sql"
        try {
            & 'sqlplus.exe' '/ as sysdba' "@$sqlFilePath" > $dbinfoFile
        } catch {
            Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] sqlplus is not found. Skipping."
        }
        return Read-PropertyFromDBInfoFile "{oracle_base_path}" $dbInfoFile
    } else {
        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] This platform is not supported yet. Skipping."
    }
}
function Get-OracleDatabaseHomePathUsingProcess {
    $thePath = $null
    $process = Get-OracleDatabaseProcess
    if (-not $process) {
        return $null
    }
    $thePath = [System.IO.Path]::GetDirectoryName($process.Path)
    if ($null -eq $thePath) {
        return $null
    }
    if ($thePath -like "*bin") {
        $thePath = ([System.IO.Path]::GetDirectoryName($process.Path)) | Split-Path -Parent
    }
    if (-not (Test-Path -Path $thePath)) {
        return $null
    }
    return $thePath
}
function Get-OracleDatabaseHomePathUsingServices {
    if ($IsWindows) {
        $service = Get-OracleDatabaseService
        if (!$service) {
            return $null
        }
        $servicePath = $service.BinaryPathName
        if ($servicePath -like "*bin*") {
            return [System.IO.Path]::GetDirectoryName(($servicePath -split "bin")[0])
        }
    }
    return $null
}
function Get-OracleDatabaseHomePathUsingRegistry {
    if ($IsWindows) {
        function Get-HomePath {
            param(
                [string]
                $RegKey
            )
            $regEntries = Get-ChildItem -Recurse -Path $RegKey
            $filteredEntries = @()
            foreach ($entry in $regEntries) {
                if ($entry.Property -contains "ORACLE_HOME") {
                    $filteredEntries += $entry
                }
            }
            if (!$filteredEntries) {
                return $null
            }
            if ($filteredEntries.Length -gt 1) {
                Write-LogCritical -ThrowException "[$( $MyInvocation.MyCommand )] Multiple version of Oracle DB detected in the Registry $($regKey)."
            }
            $thePath = (Get-ItemProperty -Path $filteredEntries.PSPath).ORACLE_HOME
            if (-not (Test-Path -Path $thePath)) {
                return $null
            }
            return [System.IO.Path]::GetDirectoryName($thePath + [System.IO.Path]::DirectorySeparatorChar)
        }
        $regKeys = @(
            "HKLM:\SOFTWARE\Oracle",
            "HKLM:\SOFTWARE\WOW6432Node\Oracle"
        )
        $thePath = $null
        foreach ($key in $regKeys) {
            if (Test-Path $key) {
                $thePath = Get-HomePath -RegKey $key
            }
            if ($thePath) {
                break
            }
        }
        if ($null -eq $thePath) {
            return $null
        }
        return $thePath
    }
    return $null
}
function Get-OracleDatabaseHomePath {
    $thePath = $null
    if ($IsWindows) {
        $thePath = Get-OracleDatabaseHomePathUsingServices
        if ($thePath) {
            return $thePath
        }
    }
    $thePath = Get-OracleDatabaseHomePathUsingProcess
    if ($thePath) {
        return $thePath
    }
    if ($IsWindows) {
        $thePath = Get-OracleDatabaseHomePathUsingRegistry
        if ($thePath) {
            return $thePath
        }
    }
    Write-LogCritical -ThrowException "[$( $MyInvocation.MyCommand )] Unable to determine Oracle DB's home path."
}
function Get-OracleDatabaseListenerProcess {
    $process = Get-Process | Where-Object { $_.ProcessName -imatch "tnslsnr" }
    if ($null -eq $process) {
        return $null
    }
    if ($proces -is [System.Array]) {
        Write-LogCritical -ThrowException -Message "[$( $MyInvocation.MyCommand )] Multiple Oracle Listener processes found. This is not expected."
    }
    return $process
}
function Get-OracleDatabaseListenerService {
    $service = Get-Service | Where-Object { ($_.Name -like "*ora*") -and ($_.BinaryPathName -like "*TNSLSNR*") }
    if (!$service) {
        $process = Get-OracleDatabaseListenerProcess
        $service = Get-CimInstance -Class Win32_Service -Filter ("ProcessId LIKE '" + $process.Id + "'")
        $service = Get-Service $service.Name
    }
    if (!$service) {
        return $null
    }
    if ($service.Length -gt 1) {
        Write-LogCritical -ThrowException -Message "[$( $MyInvocation.MyCommand )] Unable to determine the Oracle Database Listener Service. Multiple services match the search criteria. Do we have two or more listeners running on this machine?"
    }
    return $service
}
function Get-OracleDatabaseProcess {
    $process = Get-Process | Where-Object { $_.ProcessName -imatch "oracle" }
    if ($null -eq $process) {
        return $null
    }
    if ($proces -is [System.Array]) {
        Write-LogCritical -ThrowException -Message "[$( $MyInvocation.MyCommand )] Multiple Oracle processes found. This is not expected."
    }
    return $process
}
function Get-OracleDatabaseService {
    $service = Get-Service | Where-Object { ($_.Name -like "*ora*") -and ($_.BinaryPathName -like "*ORACLE.EXE*") }
    if (!$service) {
        $process = Get-OracleDatabaseProcess
        $service = Get-CimInstance -Class Win32_Service -Filter ("ProcessId LIKE '" + $process.Id + "'")
        $service = Get-Service $service.Name
    }
    if (!$service) {
        return $null
    }
    if ($service.Length -gt 1) {
        Write-LogCritical -ThrowException -Message "[$( $MyInvocation.MyCommand )] Unable to determine the Oracle DB Service. Multiple services match the search criteria. Do we have two or more Oracle.exe processes running on this machine?"
    }
    return $service
}
function Get-OracleDatabaseVersionUsingOraversionBin {
    $oracleHome = Get-OracleDatabaseHomePath
    if ($IsWindows) {
        $oraversionPath = Join-Path -Path $oracleHome -ChildPath "bin\oraversion.exe"
    }
    else {
        $oraversionPath = Join-Path -Path $oracleHome -ChildPath "bin\oraversion"
    }
    if (Test-Path $oraversionPath) {
        $versionOutput = & $oraversionPath -compositeVersion
        return $versionOutput
    }
    return $null
}
function Get-OracleDatabaseVersion {
    $oracleVersion = Get-OracleDatabaseVersionUsingOraversionBin
    if ($oracleVersion) {
        return $oracleVersion
    }
    return $null
}
function Get-OracleMetadata {
    $returnObj = @{
        Database = @{
            Process  = $null
            Service  = $null
            HomePath = $null
            Version  = $null
        }
        Listener = @{
            Process  = $null
            Service  = $null
            HomePath = $null
            Version  = $null
        }
    }
    function Get-OracleDatabaseMetadata {
        try {
            $returnObj.Database.Process = Get-OracleDatabaseProcess
        }
        catch {
            Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Unable to find the Oracle Database process."
        }
        try {
            $returnObj.Database.Service = Get-OracleDatabaseService
        }
        catch {
            Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Unable to find the Oracle Database service."
        }
        try {
            $returnObj.Database.HomePath = Get-OracleDatabaseHomePath
        }
        catch {
            Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Unable to find the Oracle Database home path."
        }
        try {
            $returnObj.Database.Version = Get-OracleDatabaseVersion
        }
        catch {
            Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Unable to determine the Oracle Database version."
        }
    }
    Get-OracleDatabaseMetadata
    function Get-OracleDatabaseListenerMetadata {
        try {
            $returnObj.Listener.Process = Get-OracleDatabaseListenerProcess
        }
        catch {
            Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Unable to find the Oracle Database Listener process."
        }
        try {
            $returnObj.Listener.Service = Get-OracleDatabaseListenerService
        }
        catch {
            Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Unable to find the Oracle Database Listener service."
        }
        $returnObj.Listener.HomePath = $returnObj.Database.HomePath
        $returnObj.Listener.Version = $returnObj.Database.Version
    }
    Get-OracleDatabaseListenerMetadata
    return $returnObj
}
function Get-OraclePluggableDbSid {
    [CmdletBinding()]
    param(
        [string]
        $BrowerPropertiesPath)
    function Get-OracleDbId {
        $dbInfoFile = Join-Path $(Get-LocalDotmaticsPath) "db-oracle-id.txt"
        $sqlFilePath = Get-ExternalFilePath -FileName "sql-get-database-id.sql"
        try {
            & 'sqlplus.exe' '/ as sysdba' "@$sqlFilePath" > $dbinfoFile
        } catch {
            Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] sqlplus is not found. Skipping."
        }
        $pdbs = Read-PropertyFromDBInfoFile "{pdbs}" $dbInfoFile
        if ($pdbs) {
            return $pdbs
        }
        $dbInfoFile = Join-Path $(Get-LocalDotmaticsPath) "db-oracle-id-vservices.txt"
        $sqlFilePath = Get-ExternalFilePath -FileName "sql-get-database-id-vservices.sql"
        try {
            & 'sqlplus.exe' '/ as sysdba' "@$sqlFilePath" > $dbinfoFile
        } catch {
            Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] sqlplus is not found. Skipping."
        }
        $pdbs = Read-PropertyFromDBInfoFile "{vservices}" $dbInfoFile
        if ($pdbs) {
            return $pdbs
        }
        Write-LogInfo  -Message "[$( $MyInvocation.MyCommand )] Database id was not read from DB!"
        return $null  
    }
    try {
        if ([string]::IsNullOrWhiteSpace($BrowerPropertiesPath)) {
            $webAppPath = Get-TomcatWebAppPath
            $BrowerPropertiesPath = Get-BrowserPlatformPropertiesFilePath -TomcatWebAppPath $webAppPath
        }
    }
    catch {
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Browser Properties file not found!"
    } 
    if (Test-Path -Path $BrowerPropertiesPath) {
        $dtxSystem = Get-DTXSystem
        $line = $dtxSystem.AppInfo.BrowserPlatform.Properties['db.description']
        if ($line -match 'SID=([A-Za-z0-9]+)\)') {
            $sid = $Matches[1]
            return $sid
        }
        if ($line -match 'SERVICE_NAME=([A-Za-z0-9]+)\)') {
            $sid = $Matches[1]
            return $sid
        }
        $line =  $dtxSystem.AppInfo.BrowserPlatform.Properties['db.description']
        if (![string]::IsNullOrWhiteSpace($line)) {
            return $line
        }
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Database sid not found!"
    }
    return Get-OracleDbId
}
function Get-PreferredDomainController {
    [cmdletbinding()]
    param(
        [Parameter(Mandatory = $true)]
        [String]$Region
    )
    $_domainControllers = (Get-Defaults).ActiveDirectory.DomainControllers
    $_activeDomainControllers = [System.Collections.ArrayList]::new()
    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Chosing an Active Directory domain controller..."
    foreach ($dc in $_domainControllers) {
        $reachable = Test-SSMReachability -InstanceId $dc.InstanceId -Region $dc.Region
        if ($reachable) {
            [void]$_activeDomainControllers.Add($dc)
        }
    }
    foreach ($adc in $_activeDomainControllers) {
        if ($adc.Region.ToLower() -eq $Region.ToLower()) {
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Chose '$( $adc.Name )' in '$( $adc.Region )' region."
            return $adc
        }
    }
    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Chose '$( $_activeDomainControllers[0].Name )' in '$( $_activeDomainControllers[0].Region )' region."
    return $_activeDomainControllers[0]
}
function Get-PRTGDeviceName {
    [cmdletbinding()]
    param (
        [Parameter(Mandatory = $true)]
        [String]$CustomerName,
        [Parameter(Mandatory = $true)]
        [String]$PRTGRegion,
        [Parameter(Mandatory = $true)]
        [String]$Environment
    )
    process {
        $name = "$PRTGRegion-$CustomerName-$Environment".ToLower()
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] PRTG device name: $name"
        return $name
    }
}
function Get-PRTGGroupId {
    [cmdletbinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [String]$PRTGRegion
    )
    process {
        try {
            $GroupId = (Get-Group -Name $PRTGRegion).Id
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] PRTG group id: $GroupId"
            return $GroupId
        }
        catch {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] $( $_.Exception.Message )" -ThrowException -Exception $_
        }
    }
}
function Get-PRTGRegion {
    [cmdletbinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]$AWSRegion
    )
    process {
        $regions = (Get-Defaults).PRTG.RegionMapping 
        if ($regions.Keys -notcontains $AWSRegion) {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] AWS region $AWSRegion is not set up in PRTG."
            throw
        }
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] PRTG region: $($regions[$AWSRegion].ToString() )"
        return $regions[$AWSRegion]
    }
}
function Get-RandomString {
    param(
        [Parameter(Mandatory = $false)]
        [int]$Length = 8
    )
    $characters = 'abcdefghijkmnpqrstuvwxyz123456789'
    $randomString = ''
    $seed = [int]((Get-Date).Ticks % [int]::MaxValue)
    $random = New-Object System.Random($seed)
    for ($i = 0; $i -lt $length; $i++) {
        $randomIndex = $random.Next(0, $characters.Length)
        $randomChar = $characters[$randomIndex]
        $randomString += $randomChar
    }
    return $randomString
}
function Get-ScheduledTasksInfo {
    $scheduedTaskObj = New-Object ScheduledTasks
    return $scheduedTaskObj.ListNonSystemScheduledTasks() | Select-Object name, status, enabled
}
function Get-SSLCertificateFingerprint {
    param(
        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [String]
        $Hostname = 'localhost',
        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [Int]
        $Port = 443,
        [Parameter(Mandatory = $false)]
        [ValidateSet('SHA1', 'SHA256', 'SHA384', 'SHA512')]
        [ValidateNotNullOrEmpty()]
        [String]
        $HashAlgorithm = 'SHA1'
    )
    return [DTXUtils]::GetSSLCertificateFingerprint($Hostname, $Port, $HashAlgorithm)
}
function Get-SSMCommandOutput {
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $CommandId,
        [Parameter(Mandatory = $true)]
        [string]
        $InstanceId,
        [Parameter(Mandatory = $true)]
        [string]
        $Region
    )
    process {
        $output = @{}
        $commandPlugins = Get-SSMCommandInvocation -InstanceId $InstanceId -CommandId $CommandId -Region $Region -Detail:$true | Select-Object -ExpandProperty CommandPlugins
        foreach ($plugin in $commandPlugins) {
            if ($plugin.Output -like "*skipped due to unsupported plugin*") {
                continue
            }
            $invokeResult = Get-SSMCommandInvocationDetail -InstanceId $InstanceId -CommandId $CommandId -Region $Region -PluginName $plugin.Name
            if (-not $invokeResult) {
                Write-LogError -Message "[$($MyInvocation.MyCommand)] No SSM command invocation result found for command id '$CommandId' with plugin name '$($plugin.Name)' in region '$Region'."
                return 
            }
            $output[$plugin.Name] = @{
                StandardOutput = $invokeResult.StandardOutputContent
                StandardError  = $invokeResult.StandardErrorContent
            }
        }
        return $output
    }
}
function Get-StateContent {
    param (
        [string]
        $StateFilePath
    )
    return (New-KeyValueStore -Path $StateFilePath).GetStoreContent()
}
function Get-StateItem {
    param(
        [Parameter(Mandatory)]
        [string]
        $Key,
        [string]
        $StateFilePath
    )
    return (New-KeyValueStore -Path $StateFilePath).GetValue($Key)
}
function Get-StringHash {
    param (
        [Parameter(Mandatory = $true)]
        [String]$String
    )
    $hasher = [System.Security.Cryptography.HashAlgorithm]::Create('sha256')
    $hash = $hasher.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($String))
    return [System.BitConverter]::ToString($hash).Replace('-', '')
}
function Get-SystemInfo {
    $returnValue = [psobject]@{
        IsWindows = $IsWindows
        IsLinux   = $IsLinux
        IsMacOS   = $IsMacOS
        Windows   = [psobject]@{
            Build            = $null
            EditionId        = $null
            InstallationType = $null
            ProductName      = $null
            Version          = $null
            VersionAsYear    = $null
        }
        Linux     = [psobject]@{
        }
        MacOS     = [psobject]@{
        }
    }
    if ($IsWindows) {
        $props = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion"
        if ($props.ProductName -match '\b\d+\b') {
            $returnValue.Windows.VersionAsYear = $matches[0] -as [int]
        }
        $returnValue.Windows.Build = $props.CurrentBuild -as [string]
        $returnValue.Windows.EditionId = $props.EditionId -as [string]
        $returnValue.Windows.InstallationType = $props.InstallationType -as [string]
        $returnValue.Windows.ProductName = $props.ProductName -as [string]
        $returnValue.Windows.Version = $props.CurrentVersion -as [version]
    }
    elseif ($IsLinux) {}
    elseif ($IsMacOS) {}
    else { throw (New-Object DTXPSException("[$( $MyInvocation.MyCommand )] Unsupported OS")) }
    return $returnValue
}
function Get-TomcatHomePathUsingProcess {
    $tomcatPath = $null
    $process = Get-TomcatProcess
    if (-not $process) {
        return $null
    }
    $tomcatPath = [System.IO.Path]::GetDirectoryName($process.Path)
    if ($null -eq $tomcatPath) {
        return $null
    }
    if ($tomcatPath -like "*bin") {
        $tomcatPath = ([System.IO.Path]::GetDirectoryName($process.Path)) | Split-Path -Parent
    }
    if (-not (Test-Path -Path $tomcatPath)) {
        return $null
    }
    return $tomcatPath
}
function Get-TomcatHomePathUsingWMI {
    $tomcatPath = $null
    $service = Get-CimInstance -Class Win32_Service | Where-Object { $_.Name -like "*tomcat*" } | Where-Object { $_.State -like "*run*" } | Select-Object StartMode, State, Name, PathName
    if ($service -is [System.Array]) {
        Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] Multiple Tomcat services found and set to automatic start. Please ensure only one Tomcat service is installed or set to start automatically." -ThrowException 
    }
    if ($null -eq $service) {
        return $null
    }
    if (-not $service.PathName) {
        return $null
    }
    $tomcatPathRaw = (($service.PathName -split "bin")[0] -replace '"', "")
    $tomcatPath = [System.IO.Path]::GetDirectoryName($tomcatPathRaw)
    if (-not (Test-Path -Path $tomcatPath)) {
        return $null
    }
    return $tomcatPath
}
function Get-TomcatHomePath {
    [OutputType([string])]
    param()
    $tomcatPath = Get-TomcatHomePathUsingProcess
    if ($null -ne $tomcatPath) {
        return $tomcatPath
    }
    $tomcatPath = Get-TomcatHomePathUsingWMI
    if ($null -ne $tomcatPath) {
        return $tomcatPath
    }
    Write-LogCritical -ThrowException -Message "[$( $MyInvocation.MyCommand )] Unable to determine Tomcat home path. Please ensure Tomcat is installed and at least one Tomcat service is set to start automatically." 
}
function Get-TomcatServerXMLPath {
    return join-path (Get-TomcatHomePath) "conf" "server.xml"
}
function Get-TomcatWebAppPath {
    [XML]$xmlfile = Get-Content (Get-TomcatServerXMLPath)
    return Join-Path (Get-TomcatHomePath) $xmlfile["Server"]["Service"]["Engine"]["Host"].appBase
}
function ConvertFrom-RawJavaVersionString {
    param (
        [Parameter(Mandatory = $true)]
        [string]$RawString
    )
    $_version = ($RawString -split "version")[1]
    $_version = $_version.Trim()
    $_version = ($_version -split " ")[0]
    $_version = $_version -replace '"', ""
    if ($_version -like "1.*") {
        $_version = $_version.Replace("0_", "")
    }
    if ($_version.Split(".").Count -eq 1) {
        $_version = $_version + ".0.0"
    }
    elseif ($_version.Split(".").Count -eq 2) {
        $_version = $_version + ".0"
    }
    elseif ($_version.Split(".").Count -eq 3) {
    }
    elseif ($_version.Split(".").Count -eq 4) {
        $_split = $_version.Split(".")
        $_version = "$($_split[0]).$($_split[1]).$($_split[3])"
    }
    else {
        Write-LogCritical -ThrowException -Message "[$( $MyInvocation.MyCommand )] Unable to determine the Java version"
    }
    return $_version
}
function Get-TomcatJavaMetadata {
    $returnObj = @{
        JavaVersion  = $null
        JavaVendor   = $null
        JavaHomePath = $null
    }
    $jvmPath = (Get-TomcatJvmPath).FullName.ToString()
    $javaHomePath = ($jvmPath -split "bin")[0]
    $javaExec = Join-Path $javaHomePath "bin" "java.exe"
    if (-not (Test-Path -Path $javaExec)) {
        Write-LogCritical -ThrowException -Message "[$( $MyInvocation.MyCommand )] Unable to locate the Java executable"
    }
    $javaVersionOutput = & $javaExec -version 2>&1
    if (-not $javaVersionOutput) {
        Write-LogCritical -ThrowException -Message "[$( $MyInvocation.MyCommand )] Unable to determine the Java version"
    }
    $javaVersionOutput = $javaVersionOutput -split "`r`n"
    if ($javaVersionOutput.Count -ne 3) {
        Write-LogCritical -ThrowException -Message "[$( $MyInvocation.MyCommand )] Unable to determine the Java version"
    }
    $javaVersionString = $javaVersionOutput[0]
    $javaRuntimeString = $javaVersionOutput[1]
    switch -Wildcard ($javaRuntimeString) {
        "*zulu*" {
            $returnObj.JavaVendor = "Azul"
            break
        }
        "*corretto*" {
            $returnObj.JavaVendor = "AmazonCorretto"
            break
        }
        "*adoptopenjdk*" {
            $returnObj.JavaVendor = "AdoptOpenJDK"
            break
        }
        "*temurin*" {
            $returnObj.JavaVendor = "AdoptOpenJDK"
            break
        }
        "*openjdk*" {
            $returnObj.JavaVendor = "OpenJDK"
            break
        }
        "*java*se*runtime*environment*" {
            $returnObj.JavaVendor = "Oracle"
            break
        }
        default {
            Write-LogCritical -ThrowException -Message "[$( $MyInvocation.MyCommand )] Unable to determine the Java vendor"
        }
    }
    $returnObj.JavaVersion = ConvertFrom-RawJavaVersionString -RawString $javaVersionString
    $returnObj.JavaHomePath = $javaHomePath
    return $returnObj
}
function Get-TomcatJavaOptions {
    $tomcatServiceName = (Get-TomcatService).Name
    $returnObj = @{
        Options               = "UNKNOWN"
        JVM                   = "UNKNOWN"
        JavaMinMemoryMB       = "UNKNOWN"
        JavaMaxMemoryMB       = "UNKNOWN"
        JavaThreadStackSizeKB = "UNKNOWN"
    }
    $tomcatParams = Get-ChildItem "HKLM:\SOFTWARE\WOW6432Node\Apache Software Foundation\Procrun 2.0\$TomcatServiceName\Parameters"
    foreach ($param in $tomcatParams) {
        if ($param.PSChildName -eq "Java") {
            $returnObj.Options = $param.GetValue("Options").split("`r`n") | Where-Object { $_ -ne "" }
            $returnObj.JVM = $param.GetValue("Jvm")
            $returnObj.JavaMinMemoryMB = [int]$param.GetValue("JvmMs")
            $returnObj.JavaMaxMemoryMB = [int]$param.GetValue("JvmMx")
            $returnObj.JavaThreadStackSizeKB = [int] $param.GetValue("JvmSs")
        }
    }
    return $returnObj
}
function Get-TomcatJvmPath {
    $tomcatJvmPath = Get-TomcatJavaOptions | Select-Object -ExpandProperty JVM
    $tomcatJvmPath = $tomcatJvmPath -replace '[|/]', '\'
    $index = $tomcatJvmPath.LastIndexOf("\bin\server\jvm.dll")
    if ($index -gt 0) {
        return Get-Item $tomcatJvmPath.Substring(0, $index)
    }
    Write-LogCritical -ThrowException -Message "[$( $MyInvocation.MyCommand )] Unable to locate the Tomcat JRE path"
}
function Get-TomcatKeyStoreFile {
    [OutputType([string])]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $TomcatHomePath
    )
    $serverXmlPath = Join-Path -Path $TomcatHomePath -ChildPath "conf" -AdditionalChildPath "server.xml"
    if (-not (Test-Path -Path $serverXmlPath)) {
        Write-LogCritical -ThrowException -Message "[$( $MyInvocation.MyCommand )] Unable to find server.xml file at $serverXmlPath"
    }
    $serverXml = [xml](Get-Content $serverXmlPath)
    $keyStorePath = $serverXml.Server.Service.Connector.KeyStoreFile
    $fullkeyStorePath = $TomcatHomePath
    if ($keyStorePath -like "*/*") {
        $pathParts = $keyStorePath -split "/"
        foreach ($pathPart in $pathParts) {
            $fullkeyStorePath = Join-Path $fullkeyStorePath $pathPart
        }
    }
    elseif ($keyStorePath -like "*\*") {
        $pathParts = $keyStorePath -split "\\"
        foreach ($pathPart in $pathParts) {
            $fullkeyStorePath = Join-Path $fullkeyStorePath $pathPart
        }
    }
    else {
        $fullkeyStorePath = Join-Path $fullkeyStorePath $keyStorePath
    }
    if ($fullkeyStorePath.EndsWith("\") -or $fullkeyStorePath.EndsWith("/")) {
        $fullkeyStorePath = $fullkeyStorePath.Substring(0, $fullkeyStorePath.Length - 1)
    }
    if (-not (Test-Path -Path $fullkeyStorePath)) {
        Write-LogCritical -ThrowException -Message "[$( $MyInvocation.MyCommand )] Unable to find key store file at $fullkeyStorePath"
    }
    $tomcatKeyStoreName = [string](Get-Defaults).Tomcat.KeyStoreName
    if ([System.IO.Path]::GetFileName($fullkeyStorePath) -ne $tomcatKeyStoreName) {
        Write-LogCritical -ThrowException -Message "[$( $MyInvocation.MyCommand )] Key store file at $fullkeyStorePath is not named $tomcatKeyStoreName. This is a potential problem. Need to investigate manually. File Found: $([System.IO.Path]::GetFileName($fullkeyStorePath))"
    }
    return $fullkeyStorePath
}
function Get-TomcatMetadata {
    $returnObj = @{
        TomcatHomePath      = $null
        TomcatVersion       = $null
        TomcatProcess       = $null
        TomcatService       = $null
        TomcatServiceUser   = $null
        TomcatServerXmlPath = $null
        TomcatJavaOptions   = $null
        TomcatJavaVersion   = $null
        TomcatJavaVendor    = $null
        TomcatJavaHomePath  = $null
        TomcatCertificate   = $null
    }
    try {
        $returnObj.TomcatHomePath = Get-TomcatHomePath
    }
    catch {
        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Unable to determine the Tomcat home path"
    }
    try {
        $returnObj.TomcatVersion = Get-TomcatVersionV2
    }
    catch {
        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Unable to determine the Tomcat version"
    }
    try {
        $returnObj.TomcatProcess = Get-TomcatProcess
    }
    catch {
        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Unable to determine the Tomcat process"
    }
    try {
        $returnObj.TomcatService = Get-TomcatService
    }
    catch {
        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Unable to determine the Tomcat service"
    }
    try {
        $returnObj.TomcatServiceUser = Get-TomcatUser
    }
    catch {
        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Unable to determine the Tomcat service user"
    }
    try {
        $returnObj.TomcatServerXmlPath = Get-TomcatServerXMLPath
    }
    catch {
        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Unable to determine the Tomcat server.xml path"
    }
    try {
        $returnObj.TomcatJavaOptions = Get-TomcatJavaOptions
    }
    catch {
        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Unable to determine the Tomcat Java options"
    }
    try {
        $returnObj.TomcatCertificate = @{
            Fingerprint = @{
                "SHA1" = Get-SSLCertificateFingerprint -Hostname "localhost" -Port 443 -HashAlgorithm "SHA1"
            }
        }
    }
    catch {
        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Unable to determine the Tomcat certificate fingerprint"
    }
    try {
        $tomcatJavaMetadata = Get-TomcatJavaMetadata
        $returnObj.TomcatJavaVersion = $tomcatJavaMetadata.JavaVersion
        $returnObj.TomcatJavaVendor = $tomcatJavaMetadata.JavaVendor
        $returnObj.TomcatJavaHomePath = $tomcatJavaMetadata.JavaHomePath
    }
    catch {
        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Unable to determine the Tomcat Java metadata"
    }
    return $returnObj
}
function Get-TomcatProcess {
    $process = Get-Process | Where-Object { $_.ProcessName -imatch ".*tomcat[0-9\.]+$" }
    if ($null -eq $process) {
        return $null
    }
    if ($proces -is [System.Array]) {
        Write-LogCritical -ThrowException -Message "[$( $MyInvocation.MyCommand )] Multiple Tomcat processes found. This is not expected."
    }
    return $process
}
function Get-TomcatService {
    $process = Get-TomcatProcess
    $service = Get-CimInstance -Class Win32_Service -Filter ("ProcessId LIKE '" + $process.Id + "'")
    $service = Get-Service $service.Name
    return $service
}
function Get-TomcatUser {
    if ($IsWindows) {
        $theUser = $null
        $tomcatUser = Get-TomcatService | Select-Object -ExpandProperty UserName
        if ($tomcatUser -match "\\") {
            $tomcatUser = $tomcatUser -split "\\" | Select-Object -Last 1
        }
        try {
            if (Get-Command "Get-LocalUser" -ErrorAction SilentlyContinue) {
                $theUser = (Get-LocalUser -Name $tomcatUser).Name
            }
            else {
                $theUser = (Get-CimInstance -ClassName Win32_UserAccount -Filter "Name='$tomcatUser'").Name
            }
        }
        catch {
            Write-LogWarning "[$($MyInvocation.MyCommand)] Unable to get the Tomcat user."
        }
        return $theUser
    }
    else {
        Write-LogInfo "[$($MyInvocation.MyCommand)] Getting the Tomcat user on Linux or MacOS is not supported yet."
    }
}   
function Get-TomcatVersion {
    [OutputType([string])]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $TomcatHomePath
    )
    if ($TomcatHomePath -notlike "*tomcat*") {
        Write-LogCritical -ThrowException -Message "[$( $MyInvocation.MyCommand )] Invalid Tomcat home path."
    }
    $version = ($TomcatHomePath -split "tomcat" | Select-Object -Last 1).Trim()
    $version = $version[0]
    if (-not $version) {
        Write-LogCritical -ThrowException -Message "[$( $MyInvocation.MyCommand )] Could not determine Tomcat version."
    }
    return $version
}
function Get-TomcatVersionUsingInstalledApps {
    $tomcatInstalledApps = Get-InstalledApps | Where-Object { $_.DisplayName -like "*tomcat*" }
    $tomcatService = Get-TomcatService
    foreach ($app in $tomcatInstalledApps) {
        if ($app.UninstallString -like "*$($tomcatService.Name)*") {
            if ($app.DisplayVersion -match '([0-9]+)\.([0-9]+)\.([0-9]+)') {
                return $Matches[0]
            }
        }
    }
    return $null
}
function Get-TomcatVersionUsingReleaseNotesFile {
    $tomcatPath = Get-TomcatHomePath
    $releaseNotesPath = Join-Path $tomcatPath "RELEASE-NOTES"
    if (Test-Path $releaseNotesPath) {
        $releaseNotes = Get-Content $releaseNotesPath -Raw
        if ($releaseNotes -match 'Apache Tomcat Version ([0-9\.]+)') {
            return $Matches[1]
        }
    }
    return $null
}
function Get-TomcatVersionUsingVersionBatchFile {
    $tomcatPath = Get-TomcatHomePath
    $versionBatchFile = Join-Path $tomcatPath "bin" "version.bat"
    $tomcatJreHomePath = (Get-TomcatJvmPath).FullName.ToString()
    $env:CATALINA_HOME = $tomcatPath
    $env:JRE_HOME = $tomcatJreHomePath
    $tempFile = [System.IO.Path]::GetTempFileName()
    & $versionBatchFile > $tempFile
    $output = Get-Content $tempFile -Raw
    if ($output -match 'Server version:\s+Apache Tomcat/([0-9\.]+)') {
        return $Matches[1]
    }
    Remove-Item $tempFile -Force
    return $null
}
function Get-TomcatVersionV2 {
    $tomcatVersion = Get-TomcatVersionUsingInstalledApps
    if (-not $tomcatVersion) {
        $tomcatVersion = Get-TomcatVersionUsingReleaseNotesFile
    }
    if (-not $tomcatVersion) {
        $tomcatVersion = Get-TomcatVersionUsingVersionBatchFile
    } 
    return $tomcatVersion
}
function Get-UserCountInfo {
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string] $BaseUrl
    )
    if (-not $BaseUrl) {
        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Unable to determine user count becasue instance URL is unknown."
        return "UNKNOWN"
    }
    $loginUrl = $BaseUrl + "/browser/login.jsp"
    try {
        $webResponse = Invoke-WebRequest -uri $loginUrl -UseBasicParsing
    }
    catch {
        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Unable to determine user count becasue invoking /browser/login.jsp failed. $( $_.Exception.Message )"
        $webResponse = $null
    }
    $loginPageOutputFile = Join-Path (Get-LocalDotmaticsPath) "login-page-full.txt"
    if ($null -ne $webResponse) {
        $webResponse.content > $loginPageOutputFile
        $s = Get-Content -Path $loginPageOutputFile | Where-Object { $_ -like '*licensee:*' }
        if ($s -match 'users:\s+(\d+\/\d+)') {
            return $Matches[0]
        }
    }
    return "UNKNOWN"
}
function Get-VirtualmemoryInfo {
    $res = [ordered]@{
        isManagedAutomatically = $null 
        initialSizeGB = "UNKNOWN"
        maxSizeGB = "UNKNOWN"
        textInfo = "UNKNOWN"
    }
    if ($IsWindows) {
        $vmAutoManged = (Get-CimInstance Win32_ComputerSystem).AutomaticManagedPagefile
        if ($vmAutoManged) {
            $res.isManagedAutomatically = $true;
            $res.textInfo = 'Virtual memory is managed automatically'
        } else {
            $res.isManagedAutomatically = $false;
            $vmInitSize = (Get-CimInstance Win32_PageFileSetting |  select -ExpandProperty InitialSize) / 1024
            $vmMaxSze = (Get-CimInstance Win32_PageFileSetting |  select -ExpandProperty MaximumSize) / 1024 
            $res.initialSizeGB = $vmInitSize
            $res.maxSizeGB = $vmMaxSze
            $res.textInfo = @"
Virtual memory GB:
Initial size: $vmInitSize
Maximum size: $vmMaxSze
"@

        }
    }
    else {
        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] This platform is not supported yet."
    }
    return $res
}
function Install-AutomationDependencies {
    [cmdletbinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $InstanceId,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $Region
    )
    process {
        try {
            Test-SSMReachability -InstanceId $InstanceId -Region $Region -SkipCommandExecution -ThrowException
            $invokeParams = @{
                Name       = "DTX-InstallAutomationDependencies"
                Region     = $Region
                InstanceId = $InstanceId
                Parameters = @{}
            }
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Installing the automation dependencies on '$InstanceId'."
            $commandId = (Invoke-SSMDocumentAndRetry @invokeParams).CommandId
            Test-SSMCommandResultV2 -CommandId $commandId -InstanceId $InstanceId -Region $Region -Wait -ThrowException | Out-Null
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] The automation dependencies have been installed successfully." 
        }
        catch {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] $( $_.Exception.Message )" -ThrowException -Exception $_
        }
    }
}
function Install-CrowdStrikeAgent {
        param(
                [Parameter(Mandatory = $true)]
                [ValidateNotNullOrEmpty()]
                [string]
                $CustomerId,
                [Parameter(Mandatory = $true)]
                [ValidateNotNullOrEmpty()]
                [string]
                $InstallationFile,
                [Parameter(Mandatory = $false)]
                [ValidateNotNullOrEmpty()]      
                [string]
                $ServiceName = "CS*Falcon*" 
        )
        if (-not $IsWindows) {
                throw (New-Object DTXPSException("[$( $MyInvocation.MyCommand )] This cmdlet is only supported on Windows."))
        }
        if (-not (Test-Path -Path $InstallationFile)) {
                throw (New-Object DTXPSException("[$( $MyInvocation.MyCommand )] The installation file '$InstallationFile' does not exist."))
        }
        $installArgs = @(
                "/install",
                "/quiet",
                "/norestart",
                "ProvNoWait=1",
                "CID=$CustomerId"
        )
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Install parameters: $installArgs "
        $installResult = Start-Process -FilePath $InstallationFile -ArgumentList $installArgs -Wait -PassThru
        if (@(0, 1638) -notcontains $installResult.ExitCode) {
                throw (New-Object DTXPSException("[$( $MyInvocation.MyCommand )] Error installing CrowdStrike Agent. Exit code: $($installResult.ExitCode)."))
        }
        Start-Sleep -Seconds 5
        Start-Service -Name $ServiceName
        if (-not (Test-IsServiceRunning -SearchTerm $ServiceName)) {
                Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] The service '$ServiceName' is not running. Installation not successful." -ThrowException 
        }
}
function Install-DuoAgent {
    [cmdletbinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $InstanceId,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $Region,
        [Parameter(Mandatory = $false, ValueFromPipeline = $true)]
        [String]
        $Name = "DuoAgent"
    )
    process {
        try {
            $defaults = Get-Defaults
            Test-SSMReachability -InstanceId $InstanceId -Region $Region -SkipCommandExecution -ThrowException
            $documentParams = @{
                RepositoryBucketName    = $defaults.AWS.S3.Buckets.SoftwareRepository.Name
                RepositoryBucketKey     = $defaults.AWS.S3.Buckets.SoftwareRepository.Objects.DuoAgentInstaller.Windows
                RepositoryBucketRegion  = (Get-S3BucketLocation -BucketName $defaults.AWS.S3.Buckets.SoftwareRepository.Name).Value
                DuoCredentials          = $defaults.AWS.SSM.ParameterStore.Parameters.DuoCredentials
                SSMParameterStoreRegion = $defaults.AWS.SSM.ParameterStore.DefaultRegion
            }
            $invokeParams = @{
                Name       = "DTX-InstallDuoAgent"
                Region     = $Region
                InstanceId = $InstanceId
                Parameters = $documentParams
            }
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Installing software package '$Name' on instance '$InstanceId'."
            $commandId = (Invoke-SSMDocumentAndRetry @invokeParams).CommandId
            Test-SSMCommandResultV2 -CommandId $commandId -InstanceId $InstanceId -Region $Region -Wait -ThrowException | Out-Null
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] '$Name' installed successfully." 
        }
        catch {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] $( $_.Exception.Message )" -ThrowException -Exception $_
        }
    }
}
function Install-OpenEye {
    param (
        [string]$Version
    )
    try {
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Installing Open Eye version $Version..."
        $openEyeDefaults = Get-OpenEyeDefaults -Version $Version
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Downloading Open Eye from S3..."
        $installFile = Get-FileFromS3 -BucketName $openEyeDefaults.DownloadSource.S3.BucketName -BucketKey $openEyeDefaults.DownloadSource.S3.BucketKey -PreserveFileName
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Downloaded to $installFile"
        $tempDir = New-TempDir
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Extracting Open Eye files to $tempDir ..."
        Expand-Archive -Path $installFile -DestinationPath $tempDir -Force
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Extracted successfully."
        $unzippedFiles = Get-ChildItem -Path $tempdir -Recurse -File
        $unzippedFilesParentPath = $unzippedFiles[0].PSParentPath
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Testing the integrity of the extracted files pre-install..."
        if (!(Test-OpenEyeIntegrity -Version $Version -TargetPath $unzippedFilesParentPath)) {
            Write-LogCritical -ThrowException "[$( $MyInvocation.MyCommand )] The Open Eye installation files are not passing integrity validation. The file checksums are not matching or files are missing. The installation is aborted. No changes have been made."
        }
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Integrity check successfull."
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Testing Mol2NameBatch Output pre-install..."
        if (!(Test-Mol2NameBatchOutput -WorkingDirectory $unzippedFilesParentPath)) {
            Write-LogCritical -ThrowException "[$( $MyInvocation.MyCommand )] The Open Eye Mol2Name Batch Output test is inconsistent. The installation is aborted. No changes have been made. Source files path: $unzippedFilesParentPath"
        }
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Mol2NameBatch check successfull."
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Copy new files to the Open Eye directory..."
        $browserGifcacheToolkitPath = Get-BrowserPlatformGifcacheToolkitPath
        foreach ($file in $unzippedFiles) {
            Copy-ItemWithRetry -Path $file.FullName -Destination $browserGifcacheToolkitPath
        }
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] File copy successfull."
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Testing the integrity of the extracted files post-install..."
        if (!(Test-OpenEyeIntegrity -Version $Version -TargetPath $browserGifcacheToolkitPath)) {
            Write-LogCritical -ThrowException "[$( $MyInvocation.MyCommand )] The Open Eye installation is not passing integrity validation. The file checksums are not matching or files are missing. Open Eye is now broken!"
        }
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Integrity check successfull."
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Testing Mol2NameBatch Output post-install..."
        if (!(Test-Mol2NameBatchOutput -WorkingDirectory $browserGifcacheToolkitPath)) {
            Write-LogCritical -ThrowException "[$( $MyInvocation.MyCommand )] The Open Eye Mol2Name Batch Output test is inconsistent. Open Eye is now broken! Source files path: $browserGifcacheToolkitPath"
        }
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Mol2NameBatch check successfull."
    }
    finally {
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Cleaning up the temp files..."
        Remove-Item -Path $installFile -Force 
        Remove-Item -Path $tempDir -Recurse -Force
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Clean up is complete."
    }
}
function Install-PowerShellCoreViaSSM {
    [cmdletbinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $InstanceId,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $Region,
        [Parameter(Mandatory)]
        [ValidateSet(
            "pre_prod",
            "prod",
            IgnoreCase = $false
        )]
        [string]
        $PatchGroup
    )
    process {
        try {
            $defaults = Get-Defaults
            Test-SSMReachability -InstanceId $InstanceId -Region $Region -SkipCommandExecution -ThrowException
            $PowerShellCoreInstallerDocumentName = $defaults.AWS.SSM.ParameterStore.Parameters.CAMPowerShellCoreInstallerDocumentName -replace "<<patch-group>>", $PatchGroup
            $EC2RoleArnDocumentName = $defaults.AWS.SSM.ParameterStore.Parameters.CAMEC2RoleArnDocumentName -replace "<<patch-group>>", $PatchGroup
            $invokeParams = @{
                Name       = (Get-SSMParameter -Name $PowerShellCoreInstallerDocumentName -Region $Region).Value
                Region     = $Region
                InstanceId = $InstanceId
                Parameters = @{
                    EC2Role                 = (Get-SSMParameter -Name $EC2RoleArnDocumentName -Region $Region).Value
                    InMaintWindow           = "False"
                    RebootRequested         = "False"
                    RebootExitCode          = "52"
                    ContinueOnErrorExitCode = "55"
                    DryRun                  = "False"
                }
            }
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Installing PowerShell Core on '$InstanceId'."
            $commandId = (Invoke-SSMDocumentAndRetry @invokeParams).CommandId
            Test-SSMCommandResultV2 -CommandId $commandId -InstanceId $InstanceId -Region $Region -Wait -ThrowException | Out-Null
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] The automation dependencies have been installed successfully." 
        }
        catch {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] $( $_.Exception.Message )" -ThrowException -Exception $_
        }
    }
}
function Install-SumoLogic {
    param (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]
        $AccessId,
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string] 
        $AccessKey,
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]
        $BucketName,
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]
        $BucketObjectKey
    )
    $ErrorActionPreference = 'Stop'
    $ProgressPreference = 'SilentlyContinue'
    $WarningPreference = 'SilentlyContinue'
    try {
        $installFile = Get-FileFromS3 -BucketName $BucketName -BucketKey $BucketObjectKey -PreserveFileName
        $dtxSystem=Get-DTXSystem
        $instanceId = $dtxSystem.GenericInfo.IdentityInfo.instanceId
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Installing $installFile for instance $instanceId ..."
        Assert-True -Condition ($null -ne $instanceId) -message "[$( $MyInvocation.MyCommand )] Instance ID was null"
        Assert-True -Condition ($instanceId.Length -gt 0) -message "[$( $MyInvocation.MyCommand )] Instance ID was length 0"
        Start-Process $installFile -Wait -ArgumentList "-q","-console","-Vsumo.accessid=$AccessId", "-Vsumo.accesskey=$AccessKey", "-Vcollector.name=$instanceId"
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] $BucketObjectKey installed."
        Start-Service -Name "sumo-collector"
    }
    finally {
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Cleaning up the temp files..."
        if ($installFile -ne $null -and (Test-Path -Path $installFile)) {
            Remove-Item -Path $installFile -Force
        }
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Clean up is complete."
    }
}
function Install-VortexWeb {
    param (
        [Parameter(Mandatory)]
        [string]$Version,
        [switch] $Force
    )
    $ErrorActionPreference = 'Stop'
    $ProgressPreference = 'SilentlyContinue'
    $WarningPreference = 'SilentlyContinue'
    try {
        $today = (Get-Date -Format "yyyy-MM-dd")
        $installStatus = Get-StateItem -Key "vortex-web-install-status"
        $installVersion = Get-StateItem -Key "vortex-web-install-version"
        Set-StateItem -Key "vortex-web-install-check-date" -Value $today
        if ( (($installStatus -eq "installed") -and ($installVersion -eq $Version)) -and !$Force) {
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Vortex Web Version [$Version] is already installed. Nothing to do."
            return
        }
        $meta = Get-DTXSystem
        $vortexWebPath = Join-Path -Path $meta.AppInfo.Tomcat.WebAppsPath -ChildPath "vortexweb"
        if (!(Test-Path $vortexWebPath)) {
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] The Vortex Web path [$vortexWebPath] is not found. Nothing to do."
            return
        }
        $vortexWebDefaults = (Get-Defaults).VortexWeb."v$Version"
        if (!$vortexWebDefaults) {
            Write-LogCritical -ThrowException "[$( $MyInvocation.MyCommand )] Unable to locate Vortex Web version '$Version' in the defaults.json file."
        }
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Installing Vortex Web version $Version ..."
        Set-StateItem -Key "vortex-web-install-status" -Value "inprogress"
        Remove-StateItem -Key "vortex-web-install-date"
        Remove-StateItem -Key "vortex-web-install-version"
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Downloading Vortex Web from S3..."
        $installFile = Get-FileFromS3 -BucketName $vortexWebDefaults.DownloadSource.S3.BucketName -BucketKey $vortexWebDefaults.DownloadSource.S3.BucketKey -PreserveFileName
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Validating the install file checksum..."
        $installFileHash = (Get-FileHash -Path $installFile -Algorithm $vortexWebDefaults.DownloadSource.S3.FileChecksum.Algorithm).Hash
        if ($installFileHash -ne $vortexWebDefaults.DownloadSource.S3.FileChecksum.Hash) {
            Write-LogCritical -ThrowException "[$( $MyInvocation.MyCommand )] The file hash of the downloaded file ($installFile) does not match the expected file hash that is defined in the defaults.json."
        }
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Backing up the Vortex Web license details..."
        $vortexWebLicensePath = Join-Path -Path $vortexWebPath -ChildPath "vortex"
        $vortexWebLicenseTempPath = Join-Path -Path (Get-LocalTempPath) -ChildPath "vortex-web-license"
        Copy-Item -Recurse -Force -Path $vortexWebLicensePath -Destination $vortexWebLicenseTempPath -ErrorAction Stop
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Stopping the Tomcat service..."
        Stop-Service -Force -Name $meta.AppInfo.Tomcat.Service.Name
        Remove-Item -Recurse -Force $vortexWebPath
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Extracting Vortex Web files to $vortexWebPath ..."
        Expand-Archive -Path $installFile -DestinationPath $vortexWebPath -Force
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Restoring the Vortex Web license details..."
        Copy-Item -Recurse -Force -Path $vortexWebLicenseTempPath/* -Destination $vortexWebLicensePath
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Starting the Tomcat service..."
        Start-Service -Name $meta.AppInfo.Tomcat.Service.Name
        Set-StateItem -Key "vortex-web-install-version" -Value $Version
        Set-StateItem -Key "vortex-web-install-date" -Value $today
        Set-StateItem -Key "vortex-web-install-status" -Value "installed"
        Remove-Item -Path $vortexWebLicenseTempPath -Force -Recurse
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Installation complete"
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Testing if the Vortex Web service is healthy."
        if (Test-HTTPService -MaxRetryCount 100 -SkipCertificateCheck -ExpectedStatusCode 200 -Uri "https://localhost/vortexweb") {
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Vortex Web is healthy."
        }
        else {
            Write-LogWarning "[$( $MyInvocation.MyCommand )] Unable to determine if Vortex Web is healthy."
        }
    }
    finally {
        if ($meta) {
            Start-Service -Name $meta.AppInfo.Tomcat.Service.Name
        }
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Cleaning up the temp files..."
        if ($installFile) {
            Remove-Item -Path $installFile -Force 
        }
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Clean up is complete."
    }
}
function Invoke-ADDomainJoin {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [String]
        $InstanceId,
        [Parameter(Mandatory = $true)]
        [String]
        $Region
    )
    process {
        try {
            Test-SSMReachability -InstanceId $InstanceId -Region $Region -SkipCommandExecution -ThrowException
            $defaults = Get-Defaults
            $documentParams = @{
                DomainName              = $defaults.ActiveDirectory.FQDN
                TargetOUPath            = $defaults.ActiveDirectory.OUPaths.ComputerGroupPaths[$Region]
                ServiceAccountUsername  = $defaults.AWS.SSM.ParameterStore.Parameters.AdServiceAccountUsername
                ServiceAccountPassword  = $defaults.AWS.SSM.ParameterStore.Parameters.AdServiceAccountPassword
                SSMParameterStoreRegion = $defaults.AWS.SSM.ParameterStore.DefaultRegion
            }
            $invokeParams = @{
                Name       = "DTX-ADDomainJoin"
                Region     = $Region
                InstanceId = $InstanceId
                Parameters = $documentParams
            }
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Joining instance '$InstanceId' to the AD domain..."
            $commandId = (Invoke-SSMDocumentAndRetry @invokeParams).CommandId
            Test-SSMCommandResultV2 -CommandId $commandId -InstanceId $InstanceId -Region $Region -Wait -ThrowException | Out-Null
            Start-Sleep -Seconds 30
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] The operation succeded!"
            Wait-ForSSMReachability -InstanceId $InstanceId -Region $Region
        }
        catch {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] $( $_.Exception.Message )" -ThrowException -Exception $_
        }
    }
}
function Invoke-PostDeploymentTasks {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $AccessUrl,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $ActiveDirectoryGroupName,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $InstanceId,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $Region
    )
    process {
        try {
            Test-SSMReachability -InstanceId $InstanceId -Region $Region -SkipCommandExecution -ThrowException
            $documentParams = @{
                AccessUrl                = $AccessUrl
                ActiveDirectoryGroupName = $ActiveDirectoryGroupName
            }
            $invokeParams = @{
                Name       = "DTX-PostDeploymentTasks"
                Region     = $Region
                InstanceId = $InstanceId
                Parameters = $documentParams
            }
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Running post-deployment tasks on '$InstanceId'."
            $commandId = (Invoke-SSMDocumentAndRetry @invokeParams).CommandId
            Test-SSMCommandResultV2 -CommandId $commandId -InstanceId $InstanceId -Region $Region -Wait -ThrowException | Out-Null
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Post-deployment tasks completed"
        }
        catch {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] $( $_.Exception.Message )" -ThrowException -Exception $_
        }
    }
}
function Invoke-SSMDocument {
    [cmdletbinding()]
    param (
        [Parameter(Mandatory = $true)]
        [String]
        $Name,
        [Parameter(Mandatory = $false)]
        [String]
        $Version = '$LATEST',
        [Parameter(Mandatory = $true)]
        [String]
        $InstanceId,
        [Parameter(Mandatory = $true)]
        [Hashtable]
        $Parameters,
        [Parameter(Mandatory = $true)]
        [String]
        $Region,
        [Parameter(Mandatory = $false)]
        [Int32]
        $TimeoutSeconds = 60,
        [Parameter(Mandatory = $false)]
        [String]
        $MaxConcurrency = "50",
        [Parameter(Mandatory = $false)]
        [String]
        $MaxErrors = "0"
    )
    $isOnlineDoc = $false
    $isUploadedDoc = $false
    $uploadedDocName = ""
    $randomString = ( -join ((65..90) + (97..122) | Get-Random -Count 5 | % { [char]$_ }))
    $sendCommandParams = @{
        DocumentVersion = $Version
        Parameters      = $Parameters
        TimeoutSeconds  = $TimeoutSeconds
        MaxConcurrency  = $MaxConcurrency
        MaxErrors       = $MaxErrors
        Region          = $Region
        InstanceId      = $InstanceId
    }
    $ssmDoc = (Get-SSMDocumentList -Region $Region -Filter @{ Key = "Name"; Values = $Name } | Select-Object -First 1)
    if ($ssmDoc.Name) {
        $isOnlineDoc = $true
    }
    else {
        try {
            $localSSMDocs = Get-ChildItem -Path $PSScriptRoot/SSMDocuments -Filter *.yml
            foreach ($localDoc in $localSSMDocs) {
                if ($Name.ToLower() -eq $localDoc.BaseName.ToLower()) {
                    $uploadedDocName = "$( $localDoc.Basename )-$randomString"
                    [void]$( New-SSMDocument -Region $Region -DocumentFormat YAML -DocumentType "Command" (Get-Content -Path $localDoc -Raw) -Name $uploadedDocName )
                    $isUploadedDoc = $true
                }
            }
        }
        catch {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] $( $_.Exception.Message )" -ThrowException -Exception $_
        }
    }
    if ($isUploadedDoc) {
        $sendCommandParams["DocumentName"] = $uploadedDocName
        $command = Send-SSMCommand @sendCommandParams
        Remove-SSMDocument -Name $uploadedDocName -Enforce:$true -Confirm:$false -Region $Region
        return $command
    }
    elseif ($isOnlineDoc) {
        $sendCommandParams["DocumentName"] = $ssmDoc.Name
        return Send-SSMCommand @sendCommandParams
    }
    else {
        Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] $( $_.Exception.Message )" -ThrowException
    }
}
function Invoke-SSMDocumentAndRetry {
    [cmdletbinding()]
    param (
        [Parameter(Mandatory = $true)]
        [String]
        $Name,
        [Parameter(Mandatory = $false)]
        [String]
        $Version = '$LATEST',
        [Parameter(Mandatory = $true)]
        [String]
        $InstanceId,
        [Parameter(Mandatory = $true)]
        [Hashtable]
        $Parameters,
        [Parameter(Mandatory = $true)]
        [String]
        $Region,
        [Parameter(Mandatory = $false)]
        [Int32]
        $TimeoutSeconds = 60,
        [Parameter(Mandatory = $false)]
        [String]
        $MaxConcurrency = "50",
        [Parameter(Mandatory = $false)]
        [String]
        $MaxErrors = "0"
    )
    function _Test-IsRetriableError {
        param (
            $CommandId,
            $InstanceId,
            $Region,
            [String[]]$RetriableErrorMessages
        )
        $commandOutput = Get-SSMCommandOutput -CommandId $CommandId -InstanceId $InstanceId -Region $Region
        foreach ($step in $commandOutput.GetEnumerator()) {
            foreach ($errorMessage in $RetriableErrorMessages) {
                if ($step.Value.StandardOutput -like $errorMessage -or $step.Value.StandardError -like $errorMessage) {
                    return $true
                }
            }
        }
        return $false
    }
    $retriableErrorMessages = @(
        "*document worker timed out*",
        "*The term 'pwsh' is not recognized as the name of a cmdlet*"
    )
    $maxRetries = 3
    for ($retryCount = 0; $retryCount -le $maxRetries; $retryCount++) {
        try {
            $invokeParams = @{
                Name           = $Name
                Version        = $Version
                InstanceId     = $InstanceId
                Parameters     = $Parameters
                Region         = $Region
                TimeoutSeconds = $TimeoutSeconds
                MaxConcurrency = $MaxConcurrency
                MaxErrors      = $MaxErrors
            }
            $command = Invoke-SSMDocument @invokeParams
            if (Test-SSMCommandResultV2 -CommandId $command.CommandId -InstanceId $InstanceId -Region $Region -Wait) {
                return $command
            }
            $isRetriable = _Test-IsRetriableError -CommandId $command.CommandId -InstanceId $InstanceId -Region $Region -RetriableErrorMessages $retriableErrorMessages
            if (-not $isRetriable) {
                return $command
            }
            Start-Sleep -Seconds 5
        }
        catch {
            Start-Sleep -Seconds 5
        }
    }
    if ($null -eq $command) {
        Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] Unable to invoke SSM document '$Name' on instance '$InstanceId' in region '$Region'." -ThrowException
    }
    return $command
}
function Write-LogInfo {
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]$Message
    )
    [DTXLogger]::LogInfo($Message)
}
function Write-LogWarning {
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]$Message
    )
    [DTXLogger]::LogWarning($Message)
}
function Write-LogError {
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]$Message
    )
    [DTXLogger]::LogError($Message)
}
function Write-LogCritical {
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]$Message,
        [Parameter(Mandatory = $false)]
        [switch]$ThrowException,
        [object] $Exception
    )
    if ($ThrowException) {
        if ($Exception) {
            [DTXLogger]::LogCriticalAndThrow($Message, $Exception)
        }
        else {
            [DTXLogger]::LogCriticalAndThrow($Message)
        }
    }
    else {
        [DTXLogger]::LogCritical($Message)
    }
}
function Write-LogSeparator {
    [DTXLogger]::LogSeparator()
}
function New-ADSecurityGroup {
    [cmdletbinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $Name,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $Description,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $DomainControllerInstanceId,
        [Parameter(Mandatory = $false, ValueFromPipeline = $true)]
        [String]
        $TargetOU,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [Array]
        $Members,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $Region
    )
    process {
        try {
            Test-SSMReachability -InstanceId $DomainControllerInstanceId -Region $Region -SkipCommandExecution -ThrowException
            $targetOu = (Get-Defaults).ActiveDirectory.OUPaths.SecurityGroupPath
            $documentParams = @{
                Name        = $Name
                Description = $Description
                Category    = "Security"
                Scope       = "DomainLocal"
                Path        = $targetOu
                GroupMember = $Members[0]
            }
            $invokeParams = @{
                Name       = "DTX-NewADGroup"
                Region     = $Region
                InstanceId = $DomainControllerInstanceId
                Parameters = $documentParams
            }
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Creating Active Directory security group '$Name' in OU '$targetOU' with group members '$Members'."
            [void]$( Invoke-SSMDocumentAndRetry @invokeParams )
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Active Directory security group created."
        }
        catch {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] $( $_.Exception.Message )" -ThrowException -Exception $_
        }
    }
}
function New-AWSEC2Instance {
    [cmdletbinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $InstanceName,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $InstanceType,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $InstanceIamProfileName,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $SubnetId,
        [Parameter(Mandatory = $true)]
        [Array]
        $SecurityGroupIds,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $AMIId,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $Region,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [Hashtable]
        $Tags
    )
    process {
        try {
            $instanceCheck = (Get-EC2Instance -Region $Region -Filter @{ Name = "tag:Name"; Values = $InstanceName }, @{ Name = "instance-state-code"; Values = "0", "16", "64", "80" }).Instances
            if ($instanceCheck.Count -gt 1) {
                Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] Multiple instances with the name '$InstanceName' are found. Please resolve this problem before proceeding."
                throw
            }
            if ($instanceCheck.Count -eq 1) {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] The AWS EC2 instance '$InstanceName' in region '$Region' already exists."
                if (Read-BasicUserResponse -Prompt "Do you want to reuse the AWS EC2 instance '$InstanceName'? (y/n)") {
                    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] AWS EC2 instance '$InstanceName' in region '$Region' will be reused."
                    return $instanceCheck[0]
                }
                else {
                    Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] You can either reuse the AWS EC2 instance '$InstanceName' or delete it and let the script recreate it. Please ensure the EC2 instance is not in use and does not have customer data before deleting it."
                    throw
                }
            }
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Creating AWS EC2 instance '$InstanceName' in region '$Region'..."
            $ec2CreateInstanceParams = @{
                Region                  = $Region
                ImageId                 = $AMIId
                MinCount                = 1
                MaxCount                = 1
                SubnetId                = $SubnetId
                InstanceType            = $InstanceType
                IamInstanceProfile_Name = $InstanceIamProfileName
                SecurityGroupId         = $SecurityGroupIds
                DisableApiTermination   = $true
            }
            $ec2Instance = (New-EC2Instance @ec2CreateInstanceParams).Instances
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] AWS EC2 instance created: $( $ec2Instance.InstanceId )"
            $counter = 0
            $ec2InstanceStatusChecksInProgress = $true
            do {
                if ($counter -ge 61) {
                    Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] The AWS EC2 instance '$InstanceName' failed to reach 'running' state or to pass the reachability tests within a 10 minute period. Please try again."
                    throw
                }
                $status = Get-EC2InstanceStatus -Region $Region -InstanceId $ec2Instance.InstanceId
                if ($status.InstanceState.Code -eq 16 -and $status.Status.Status.Value -eq "ok") {
                    $ec2InstanceStatusChecksInProgress = $false
                    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] The AWS EC2 instance '$InstanceName' is ready."
                }
                else {
                    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Waiting for the AWS EC2 instance '$InstanceName' to transition to 'running' state and to pass the reachability tests."
                    Start-Sleep -Seconds 10
                }
                $counter += 1
            } while ($ec2InstanceStatusChecksInProgress)
            $Tags.Add("Name", $InstanceName)
            $Tags.Keys | ForEach-Object {
                [void]$( New-EC2Tag -Region $Region -ResourceId $ec2Instance.InstanceId -Tag @{ Key = $_; Value = $Tags[$_] } )
            }
            Restart-EC2Instance -InstanceId $ec2Instance.InstanceId -Region $Region
            return $ec2instance
        }
        catch {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] $( $_.Exception.Message )" -ThrowException -Exception $_
        }
    }
}
function New-AWSElasticIP {
    [cmdletbinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $InstanceName,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $InstanceId,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $Region,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [Hashtable]
        $Tags,
        [Switch]
        $AttachToEC2Instance
    )
    process {
        try {
            $elasticIpName = $InstanceName
            $elasticIpCheck = Get-EC2Address -Region $Region -Filter @{ Name = "tag:Name"; Values = $elasticIpName }
            if ($elasticIpCheck.InstanceId -eq $InstanceId) {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] The AWS elastic IP '$elasticIpName' in region '$Region' is already attached to instance '$InstanceName'."
                return $elasticIpCheck
            }
            if ($elasticIpCheck) {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] The AWS elastic IP '$elasticIpName' in region '$Region' already exists."
                if (Read-BasicUserResponse -Prompt "Do you want to reuse the AWS elastic IP '$elasticIpName'? (y/n)") {
                    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] AWS elastic IP '$elasticIpName' in region '$Region' will be reused."
                    if ($AttachToEC2Instance) {
                        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Attaching elastic IP '$elasticIpName' in region '$Region' to instance '$InstanceName' ($InstanceId)..."
                        [void]$( Register-EC2Address -InstanceId $InstanceId -AllocationId $elasticIpCheck.AllocationId -Region $Region )
                        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Attached."
                    }
                    return $elasticIpCheck
                }
                else {
                    Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] You can either reuse the AWS elastic IP '$elasticIpName' or delete it and let the script recreate it. Please ensure the elastic IP is not in use before deleting it."
                    throw
                }
            }
            else {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Creating AWS elastic IP '$elasticIpName' in region '$Region'..."
                $elasticIp = New-EC2Address -Region $Region -Domain Vpc
                $Tags.Add("Name", $elasticIpName)
                $Tags.Keys | ForEach-Object {
                    [void]$( New-EC2Tag -Region $Region -ResourceId $elasticIp.AllocationId -Tag @{ Key = $_; Value = $Tags[$_] } )
                }
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] AWS elastic IP '$elasticIpName' in region '$Region' created sucessfully"
                if ($AttachToEC2Instance) {
                    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Attaching elastic IP '$elasticIpName' in region '$Region' to instance '$InstanceName' ($InstanceId)..."
                    [void]$( Register-EC2Address -InstanceId $InstanceId -AllocationId $elasticIp.AllocationId -Region $Region )
                    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Attached."
                }
                return $elasticIp
            }
        }
        catch {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] $( $_.Exception.Message )" -ThrowException -Exception $_
        }
    }
}
function New-AWSRoute53ARecord {
    [cmdletbinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $Name,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $IPAddress,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $HostedZoneId,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $Region,
        [Parameter(Mandatory = $false, ValueFromPipeline = $true)]
        [Int]
        $TTL = 300
    )
    process {
        try {
            $hostedZoneName = (Get-R53HostedZone -Id $HostedZoneId -Region $Region).HostedZone.Name.Trim(".")
            $recordName = $Name.ToLower()
            $recordCheck = Get-AWSRoute53Record -Name $recordName -Type A -HostedZoneId $HostedZoneId -Region $Region
            if ($recordCheck) {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] 'A' record with name '$recordName' already exists in the hosted zone '$hostedZoneName' ($HostedZoneId)"
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] The 'A' record '$( $recordCheck.Name )' points to IP address '$( $recordCheck.ResourceRecords.Value )'"
                if ($recordCheck.ResourceRecords.Value -contains $IPAddress) {
                    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] No changes required."
                    return $recordCheck
                }
                else {
                    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] This is different from the IP address requested '$IPAddress'"
                    if (Read-BasicUserResponse -Prompt "Do you want to replace the existing IP address '$( $recordCheck.ResourceRecords.Value )' for '$( $recordCheck.Name )' with '$IPAddress'? (y/n)") {
                        if (Read-BasicUserResponse -Prompt "Are you sure? (y/n)") {
                            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Updating the record '$( $recordCheck.Name )' to point to IP address '$IPAddress'..."
                            $updatedRecord = New-Object Amazon.Route53.Model.Change
                            $updatedRecord.Action = "UPSERT"
                            $updatedRecord.ResourceRecordSet = New-Object Amazon.Route53.Model.ResourceRecordSet
                            $updatedRecord.ResourceRecordSet.Name = "$recordName.$hostedZoneName."
                            $updatedRecord.ResourceRecordSet.Type = "A"
                            $updatedRecord.ResourceRecordSet.TTL = $TTL
                            $updatedRecord.ResourceRecordSet.ResourceRecords.Add(@{ Value = $IPAddress })
                            [void]$( Edit-R53ResourceRecordSet -HostedZoneId $HostedZoneId -ChangeBatch_Change $updatedRecord -ChangeBatch_Comment "Updated on $( Get-Date -f -- FileDateTimeUniversal )" )
                            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Record updated!"
                            return $updatedRecord.ResourceRecordSet
                        }
                    }
                }
            }
            else {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Creating 'A' record '$recordName' with IP address '$IPAddress' on hosted zone '$hostedZoneName' ($HostedZoneId)..."
                $newRecord = New-Object Amazon.Route53.Model.Change
                $newRecord.Action = "CREATE"
                $newRecord.ResourceRecordSet = New-Object Amazon.Route53.Model.ResourceRecordSet
                $newRecord.ResourceRecordSet.Name = "$recordName.$hostedZoneName."
                $newRecord.ResourceRecordSet.Type = "A"
                $newRecord.ResourceRecordSet.TTL = $TTL
                $newRecord.ResourceRecordSet.ResourceRecords.Add(@{ Value = $IPAddress })
                [void]$( Edit-R53ResourceRecordSet -HostedZoneId $HostedZoneId -ChangeBatch_Change $newRecord -ChangeBatch_Comment "Added on $( Get-Date -f -- FileDateTimeUniversal )" )
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Record created!"
                return $newRecord.ResourceRecordSet
            }
        }
        catch {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] $( $_.Exception.Message )" -ThrowException -Exception $_
        }
    }
}
function New-AWSRoute53CnameRecord {
    [cmdletbinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $Name,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $Target,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $HostedZoneId,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $Region,
        [Parameter(Mandatory = $false, ValueFromPipeline = $true)]
        [Int]
        $TTL = 300
    )
    process {
        try {
            $hostedZoneName = (Get-R53HostedZone -Id $HostedZoneId -Region $Region).HostedZone.Name.Trim(".")
            $recordName = $Name.ToLower()
            $recordCheck = Get-AWSRoute53Record -Name $recordName -Type CNAME -HostedZoneId $HostedZoneId -Region $Region
            if ($recordCheck) {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] 'CNAME' record with name '$recordName' already exists in the hosted zone '$hostedZoneName' ($HostedZoneId)"
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] The 'CNAME' record '$( $recordCheck.Name )' points to '$( $recordCheck.ResourceRecords.Value )'"
                if ($recordCheck.ResourceRecords.Value -contains $Target) {
                    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] No changes required."
                    return $recordCheck
                }
                else {
                    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] This is different from the target requested '$Target'"
                    if (Read-BasicUserResponse -Prompt "Do you want to replace the existing target '$( $recordCheck.ResourceRecords.Value )' for '$( $recordCheck.Name )' with '$Target'? (y/n)") {
                        if (Read-BasicUserResponse -Prompt "Are you sure? (y/n)") {
                            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Updating the record '$( $recordCheck.Name )' to point to '$Target'..."
                            $updatedRecord = New-Object Amazon.Route53.Model.Change
                            $updatedRecord.Action = "UPSERT"
                            $updatedRecord.ResourceRecordSet = New-Object Amazon.Route53.Model.ResourceRecordSet
                            $updatedRecord.ResourceRecordSet.Name = "$recordName.$hostedZoneName."
                            $updatedRecord.ResourceRecordSet.Type = "CNAME"
                            $updatedRecord.ResourceRecordSet.TTL = $TTL
                            $updatedRecord.ResourceRecordSet.ResourceRecords.Add(@{ Value = $Target })
                            [void]$( Edit-R53ResourceRecordSet -HostedZoneId $HostedZoneId -ChangeBatch_Change $updatedRecord -ChangeBatch_Comment "Updated on $( Get-Date -f -- FileDateTimeUniversal )" )
                            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Record updated!"
                            return $updatedRecord.ResourceRecordSet
                        }
                    }
                }
            }
            else {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Creating 'CNAME' record '$recordName' with target '$Target' on hosted zone '$hostedZoneName' ($HostedZoneId)..."
                $newRecord = New-Object Amazon.Route53.Model.Change
                $newRecord.Action = "CREATE"
                $newRecord.ResourceRecordSet = New-Object Amazon.Route53.Model.ResourceRecordSet
                $newRecord.ResourceRecordSet.Name = "$recordName.$hostedZoneName."
                $newRecord.ResourceRecordSet.Type = "CNAME"
                $newRecord.ResourceRecordSet.TTL = $TTL
                $newRecord.ResourceRecordSet.ResourceRecords.Add(@{ Value = $Target })
                [void]$( Edit-R53ResourceRecordSet -HostedZoneId $HostedZoneId -ChangeBatch_Change $newRecord -ChangeBatch_Comment "Added on $( Get-Date -f -- FileDateTimeUniversal )" )
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Record created!"
                return $newRecord.ResourceRecordSet
            }
        }
        catch {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] $( $_.Exception.Message )" -ThrowException -Exception $_
        }
    }
}
function New-AWSSecurityGroup {
    [cmdletbinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $Name,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $Description,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $VPCId,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $Region,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [Hashtable]
        $Tags
    )
    process {
        try {
            $sgNameCheck = Get-EC2SecurityGroup -Region $Region -Filter @{ Name = "group-name"; Values = $Name }, @{ Name = "vpc-id"; Values = $VPCId }
            if ($sgNameCheck) {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] The AWS security group '$Name' in region '$Region' already exists."
                if (Read-BasicUserResponse -Prompt "Do you want to reuse the AWS security group '$Name'? (y/n)") {
                    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] AWS security group '$Name' in region '$Region' will be reused."
                    return $sgNameCheck.GroupId
                }
                else {
                    Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] You can either reuse the AWS security group '$Name' or delete it and let the script recreate it. Please ensure the security group is not in use before deleting it."
                    throw
                }
            }
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Creating AWS security group '$Name' in region '$Region'..."
            $sgId = New-EC2SecurityGroup -Region $Region -GroupName $Name -Description $Description -VpcId $VPCId
            $Tags.Add("Name", $Name)
            $Tags.Keys | ForEach-Object {
                [void]$( New-EC2Tag -Region $Region -ResourceId $sgId -Tag @{ Key = $_; Value = $Tags[$_] } )
            }
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] AWS security group '$Name' in region '$Region' created sucessfully"
            return $sgId
        }
        catch {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] $( $_.Exception.Message )" -ThrowException -Exception $_
        }
    }
}
$script:ExternalFilesProviderInstance = $null
function New-ExternalFilesProvider {
    if (-not $script:ExternalFilesProviderInstance) {
        $script:ExternalFilesProviderInstance = [DTXExternalFilesProvider]::new($MyInvocation)
    }
    return $script:ExternalFilesProviderInstance
}
function New-KeyValueStore {
    param(
        [string]
        $Path
    )
    if (!$Path) {
        $Path = Get-LocalStateFilePath
    }
    return [DTXKeyValueStore]::new($Path)
}
function New-PRTGDevice {
    [cmdletbinding()]
    param (
        [Parameter(Mandatory = $true)]
        [String]$Name,
        [Parameter(Mandatory = $true)]
        [String]$GroupId,
        [Parameter(Mandatory = $true)]
        [String]$TemplateDeviceId
    )
    process {
        try {
            $deviceCheck = Get-Device | Where-Object { $_.Name.ToLower() -eq $Name.ToLower() }
            if ($deviceCheck.Count -gt 1) {
                Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] More than one PRTG device with the name '$Name' exists. This is an edge case. Please make sure no more than one device with the same name exists."
                throw
            }
            if ($deviceCheck) {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] PRTG device named $Name already exists..."
                if (Read-BasicUserResponse -Prompt "Do you want to reuse the PRTG device '$( $deviceCheck.Name )' (ID: $( $deviceCheck.Id ))? (y/n)") {
                    return $deviceCheck
                }
                else {
                    Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] You can either reuse the PRTG device '$( $deviceCheck.Name )' (ID: $( $deviceCheck.Id )) or delete it and let the script recreate it. Please ensure the device is not in use before deleting it."
                    throw
                }
            }
            else {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Creating a new PRTG device..."
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Device name: $Name"
                $result = Clone-Object -SourceId $TemplateDeviceId -DestinationId $GroupId -Name $Name
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Created!"
                return $result
            }
        }
        catch {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] $( $_.Exception.Message )" -ThrowException -Exception $_
        }
    }
}
function New-TempDir {
    return New-Item -ItemType Directory -Path ([System.IO.Path]::GetTempPath()) -Name (Get-RandomString -Length 18) -Force
}
function Read-AllPropertiesFromDBInfoFile {
    param (
        [string]
        $DbInfoFile)
    if ([string]::IsNullOrEmpty($DbInfoFile)) {
        $DbInfoFile = Get-DBInfoFilePath
    }
    $res = [ordered]@{}
    $foundPlaceholder = "";
    $value = ""
    $valueReadingStarted = $false;
    $resultLines = 0;
    if (-not ([System.IO.File]::Exists($DbInfoFile))) {
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] DB info file not found"
        return $null;
    }
    foreach ($line in Get-Content $DbInfoFile) {
        if ( $line.StartsWith("#") -or [string]::IsNullOrWhiteSpace($line)) {
            continue;
        }
        if ($line -like "{end}") {
            $res.Add($foundPlaceholder, $value);
            return $res;
        }
        if ($line -like "{*}") {   
            if ($valueReadingStarted) {
                $res.Add($foundPlaceholder, $value); 
                $resultLines = 0;
                $value = ""
            }
            else {
                $valueReadingStarted = $true;
            } 
            $foundPlaceholder = $line;
            if ($line -like "{end_*}") {
                $valueReadingStarted = $false;
            }
        }
        else {
            if ($valueReadingStarted) {
                $resultLines++;
                if ($resultLines -gt 1) {
                    $value = $value + "`n" + $line.Trim();
                }
                else {
                    $value = $line.Trim();
                }
            }
        }
    }
}
function Read-PropertyFromDBInfoFile {
    param (
        [string]    
        $Placeholder,
        [string]
        $DbInfoFile)
    if ([string]::IsNullOrEmpty($DbInfoFile)) {
        $DbInfoFile = Get-DBInfoFilePath
    }
    $res = ""
    $dbPlaceHolderFound = $false;
    $resultLines = 0;
    if (-not ([System.IO.File]::Exists($DbInfoFile))) {
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] DB info file not found"
        return $null;
    }
    foreach ($line in Get-Content $DbInfoFile) {
        if ($line -like $Placeholder) {
            $dbPlaceHolderFound = $true;
            continue;
        }
        if ($dbPlaceHolderFound) {
            if ( $line.StartsWith("#") -or [string]::IsNullOrWhiteSpace($line)) {
                continue;
            }
            if ($line -like "{*}") {
                return $res;
            }
            $resultLines++;
            if ($resultLines -gt 1) {
                $res = $res + "`n" + $line.Trim();
            }
            else {
                $res = $line.Trim();
            }
        }
    }
}
function Read-BasicUserResponse {
    [cmdletbinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]$Prompt
    )
    Do {
        Write-Host -ForegroundColor Yellow $Prompt
        $answer = (Read-Host).ToLower()
        switch ($answer) {
            "y" {
                return $true
            }
            "n" {
                return $false
            }
        }
    }
    while ($answer -ne "y" -or $answer -ne "n")
}
function Remove-OpenEyeBackup {
    param (
        [Parameter(Mandatory)]
        [int]$BackupsToKeep
    )
    try {
        Write-LogInfo -Message "[$($MyInvocation.MyCommand)] Removing all Open Eye backups, except the last $BackupsToKeep."
        Push-Location -Path (Get-BrowserPlatformGifcachePath)
        $backups = (Get-ChildItem -Path "*.bak.2*.zip" | Sort-Object -Property "LastWriteTime" -Descending) | Select-Object -Skip $BackupsToKeep
        if (!$backups) {
            Write-LogInfo -Message "[$($MyInvocation.MyCommand)] No backup files found for removal."
            return
        }
        Write-LogInfo -Message "[$($MyInvocation.MyCommand)] $($backups.Count) backups selected for removal."
        $backups | Remove-Item -Force
        Write-LogInfo -Message "[$($MyInvocation.MyCommand)] $($backups.Count) backups removed"
    }
    finally {
        Pop-Location
    }
}
function Remove-StateItem {
    param(
        [Parameter(Mandatory)]
        [string]
        $Key,
        [string]
        $StateFilePath
    )
    return (New-KeyValueStore -Path $StateFilePath).RemoveKey($Key)
}
function Restart-TomcatService {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [ValidateSet(6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)]
        [int]$Version,
        [Parameter(Mandatory = $false)]
        [int]$WaitBeforeSeconds = 0,
        [Parameter(Mandatory = $false)]
        [int]$WaitAfterSeconds = 0
    )
    $WarningPreference = 'SilentlyContinue'
    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Restarting Tomcat..."
    if ($WaitBeforeSeconds -gt 0) {
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Waiting $WaitBeforeSeconds seconds before restarting Tomcat."
        Start-Sleep -Seconds $WaitBeforeSeconds
    }
    Restart-Service -Name "Tomcat$Version" -Force
    if ($WaitAfterSeconds -gt 0) {
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Waiting $WaitAfterSeconds seconds after restarting Tomcat."
        Start-Sleep -Seconds $WaitAfterSeconds
    }
    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Tomcat has been restarted successfully."
}
function Set-AWSEC2InstanceProfile {
    [cmdletbinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $Region,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $InstanceId,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $InstanceIamProfileName
    )
    process {
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Checking EC2 IAM Instance Profile..."
        try {
            $instanceRoleDetails = (Get-EC2IamInstanceProfileAssociation -Filter @{ Name = "instance-id"; Values = $InstanceId } -Region $Region)
            if ($instanceRoleDetails.IamInstanceProfile.Arn -like "*" + $InstanceIamProfileName) {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] The EC2 Instance Profile is already set to '$InstanceIamProfileName'"
                return
            }
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Setting EC2 Instance Profile to '$InstanceIamProfileName'"
            [void]$( Set-EC2IamInstanceProfileAssociation -AssociationId $instanceRoleDetails.AssociationId -IamInstanceProfile_Name $InstanceIamProfileName -Region $Region )
        }
        catch {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] $( $_.Exception.Message )" -ThrowException -Exception $_
        }
    }
}
function Set-PRTGDevice {
    [cmdletbinding()]
    param (
        [Parameter(Mandatory = $true)]
        [String]$Id,
        [Parameter(Mandatory = $false)]
        [String]$IPAddress,
        [Parameter(Mandatory = $false)]
        [String]$ServiceUrl,
        [Switch]$Resume,
        [Switch]$Pause,
        [Switch]$UpdateSensors
    )
    process {
        try {
            $device = Get-Device -Id $Id
            if (-not$device) {
                Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] The PRTG device with Id $Id does not exist. Please try again later..."
                throw
            }
            if ($IPAddress) {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Updating IP address (DNS Name) to: $IPAddress for device '$( $device.Name )' (ID: $( $device.Id ))"
                [void]$( $device | Set-ObjectProperty -Hostv4 $IPAddress )
            }
            if ($ServiceUrl) {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Updating service url to: $ServiceUrl for device '$( $device.Name )' (ID: $( $device.Id ))"
                [void]$( $device | Set-ObjectProperty -ServiceUrl $ServiceUrl )
            }
            if ($Pause) {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Pausing PRTG device '$( $device.Name )' (ID: $( $device.Id ))"
                [void]$( Pause-Object -Id $device.Id )
            }
            if ($Resume) {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Resuming PRTG device '$( $device.Name )' (ID: $( $device.Id ))"
                [void]$( Resume-Object -Id $device.Id )
            }
        }
        catch {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] $( $_.Exception.Message )" -ThrowException -Exception $_
        }
    }
}
function Set-PRTGDeviceSensor {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [String]$DeviceId,
        [Parameter(Mandatory = $true)]
        [String]$ServiceUrl,
        [Switch]$UseDefaults
    )
    process {
        try {
            if ($UseDefaults) {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Configuring PRTG sensors for device '$DeviceId'."
                $deviceSensors = Get-Sensor -Filter (New-SearchFilter -Property ParentId -Operator eq -Value $DeviceId)
                if (-not $deviceSensors) {
                    Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] The PRTG device with Id $DeviceId does not have any sensors configured. Please try again later..." -ThrowException
                }
                $sensorDefaults = (Get-Defaults).PRTG.Sensors 
                foreach ($sensorName in $sensorDefaults.Keys) {
                    $sensorConf = $sensorDefaults[$sensorName]
                    foreach ($deviceSensor in $deviceSensors) {
                        if ($deviceSensor.Name.ToLower() -eq $sensorConf.Name.ToLower()) {
                            if ($sensorConf.Action -eq "replace-service-url") {
                                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Setting the service url for PRTG sensor '$( $deviceSensor.Name )'."
                                if ($sensorConf.IsRawProperty) {
                                    [void]$( Set-ObjectProperty -Id $deviceSensor.Id -RawProperty $sensorConf.PropertyName -RawValue ($sensorConf.PropertyValueTemplate -replace ("<service_url>", $ServiceUrl)) -Force )
                                }
                                else {
                                    [void]$( Set-ObjectProperty -Id $deviceSensor.Id -Property $sensorConf.PropertyName -Value ($sensorConf.PropertyValueTemplate -replace ("<service_url>", $ServiceUrl)) )
                                }
                            }
                            if ($sensorConf.Action -eq "pause") {
                                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Pausing PRTG sensor '$( $deviceSensor.Name )' for '$( [int]$sensorConf.DurationInMinutes / 60 )' hours."
                                [void]$( Pause-Object -Id $deviceSensor.Id -Duration $sensorConf.DurationInMinutes )
                            }
                        }
                    }
                }
            }
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] PRTG sensor configuration complete."
        }
        catch {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] $( $_.Exception.Message )" -ThrowException -Exception $_
        }
    }
}
function Set-StateItem {
    param(
        [Parameter(Mandatory)]
        [string]
        $Key,
        [Parameter(Mandatory)]
        [object]
        $Value,
        [string]
        $StateFilePath
    )
    Write-LogInfo -Message "[$($MyInvocation.MyCommand)] Updating the local state file for key: $Key"
    $result = (New-KeyValueStore -Path $StateFilePath).SetValue($Key, $Value)
    Write-LogInfo -Message "[$($MyInvocation.MyCommand)] State file updated successfully."
    return $result
}
function Set-TimeZone {
    [cmdletbinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $InstanceId,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $Region
    )
    process {
        try {
            Test-SSMReachability -InstanceId $InstanceId -Region $Region -SkipCommandExecution -ThrowException
            $documentParams = @{}
            $invokeParams = @{
                Name       = "DTX-SetTimeZone"
                Region     = $Region
                InstanceId = $InstanceId
                Parameters = $documentParams
            }
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Setting the time zone on instance '$InstanceId'."
            $commandId = (Invoke-SSMDocumentAndRetry @invokeParams).CommandId
            Test-SSMCommandResultV2 -CommandId $commandId -InstanceId $InstanceId -Region $Region -Wait -ThrowException | Out-Null
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Time zone set successfully." 
        }
        catch {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] $( $_.Exception.Message )" -ThrowException -Exception $_
        }
    }
}
function Set-WindowsHostname {
    [cmdletbinding()]
    param(
        [Parameter(Mandatory = $true)]
        [String]
        $Name,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $InstanceId,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $Region
    )
    process {
        try {
            Test-SSMReachability -InstanceId $InstanceId -Region $Region -SkipCommandExecution -ThrowException
            $invokeParams = @{
                Name       = "DTX-SetWindowsHostname"
                Region     = $Region
                InstanceId = $InstanceId
                Parameters = @{
                    Name = $Name
                }
            }
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Setting the hostname of instance '$InstanceId' to '$Name'..."
            $commandId = (Invoke-SSMDocumentAndRetry @invokeParams).CommandId
            Test-SSMCommandResultV2 -CommandId $commandId -InstanceId $InstanceId -Region $Region -Wait -ThrowException | Out-Null
            Start-Sleep -Seconds 30
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] The hostname was set successfully." 
            Wait-ForSSMReachability -InstanceId $InstanceId -Region $Region
        }
        catch {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] $( $_.Exception.Message )" -ThrowException -Exception $_
        }
    }
}
function Show-Banner {
    param(
        [Parameter(Mandatory)]
        [ValidateScript({ $_.Length -le 128 })]
        [string]$Message,
        [switch]$AsLog
    )
    $count = $Message.Length + 4
    $startBar = "#" * $count
    $endBar = "#" * $count
    $payload = "# $Message #"
    if ($AsLog) {
        Write-LogInfo -Message $startBar
        Write-LogInfo -Message $payload
        Write-LogInfo -Message $endBar
    }
    else {
        Write-Host $startBar
        Write-Host $payload
        Write-Host $endBar
    }
}
function Start-TranscriptLogging {
    $transcriptFile = "$( Get-Random ).txt"
    $transcriptFilePath = Join-Path -Path $([System.IO.Path]::GetTempPath() ) -ChildPath $transcriptFile
    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Transcript started, output file is $transcriptFilePath"
    [void]$( Start-Transcript -Path $transcriptFilePath )
}
function Stop-ServiceWithRetry {
    param(
        [String]
        $Name,
        [Int]
        $Retries = 6,
        [Int]
        $WaitSeconds = 10,
        [switch]
        $Force
    )
    $count = 0
    while ($count -le $Retries) {
        $service = Get-Service -Name $Name -ErrorAction SilentlyContinue
        if (!$service) {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] A service with the name $Name is not found." -ThrowException
        }
        if ($service.Status -eq "Stopped") {
            break
        }
        if ($service.Status -eq "StopPending") {
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Service is stopping..."
        }
        else {
            if ($Force) {
                Stop-Service -Name $service.Name -Force -ErrorAction SilentlyContinue -WarningAction SilentlyContinue -NoWait | Out-Null
            }
            else {
                Stop-Service -Name $service.Name -ErrorAction SilentlyContinue -WarningAction SilentlyContinue -NoWait | Out-Null
            }
        }
        Start-Sleep -Seconds $WaitSeconds
        $count++
    }
    $service = Get-Service -Name $Name -ErrorAction SilentlyContinue
    if ($service.Status -eq "Stopped") {
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Service stopped"
        return
    }
    Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] The service $Name failed to stop within the specified retry timeout of $($Retries * $WaitSeconds) seconds." -ThrowException
}
function Sync-StarDotmaticsNetCertificate {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param()
    function Test-ChangeRequired {
        param(
            [string]$CertPath,
            [hashtable]$CertMetadata
        )
        if (!(Test-Path $CertPath)) {
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Certificate path '$CertPath' does not exist. Changes required."
            return $true
        }
        try {
            Push-Location -Path $CertPath
            $checksumAlgorithm = $CertMetadata.DownloadSource.S3.FileChecksumAlgorithm
            $expectedFiles = $CertMetadata.DownloadSource.S3.Files
            $changesRequired = $false
            foreach ($item in $expectedFiles.GetEnumerator()) {
                $fileName = $item.Key
                $expectedFileHash = $item.Value
                if (!(Test-Path $fileName)) {
                    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] File '$fileName' is missing. Changes required."
                    $changesRequired = $true
                    break
                }
                $currentFileHash = (Get-FileHash -Algorithm $checksumAlgorithm -Path $fileName).Hash
                if ($currentFileHash -ne $expectedFileHash) {
                    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] File '$fileName' has an incorrect checksum. Changes required."
                    $changesRequired = $true
                    break
                }
            }
            return $changesRequired
        }
        finally {
            Pop-Location
        }
    }
    function New-CertPath {
        param (
            [string]$CertPath
        )
        if (!(Test-Path $CertPath)) {
            if ($PSCmdlet.ShouldProcess($CertPath, "Create the certificate path")) {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Certificate path '$CertPath' does not exist. Creating..."
                New-Item -Path $CertPath -ItemType Directory -Force -ErrorAction Stop | Out-Null
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Certificate path '$CertPath' created."
            }
        }
    }
    function Get-CertFilesFromS3 {
        param(
            [string]$CertPath,
            [hashtable]$CertMetadata
        )
        $bucketName = $CertMetadata.DownloadSource.S3.BucketName
        $bucketPrefix = $CertMetadata.DownloadSource.S3.BucketPrefix
        $filesToDownload = $CertMetadata.DownloadSource.S3.Files
        $checksumAlgorithm = $CertMetadata.DownloadSource.S3.FileChecksumAlgorithm
        try {
            if ($PSCmdlet.ShouldProcess($CertPath, "Download the certificate files")) {
                Push-Location -Path $CertPath
            }
            foreach ($item in $filesToDownload.GetEnumerator()) {
                $fileName = $item.Key
                $expectedFileHash = $item.Value
                if ($PSCmdlet.ShouldProcess("s3://$bucketName/$bucketPrefix/$fileName", "Download the file")) {
                    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Downloading file 's3://$bucketName/$bucketPrefix/$fileName' to path '$CertPath'..."
                    $downloadedFile = [DTXS3Utils]::GetFile($bucketName, "$bucketPrefix/$fileName", (Get-Location), $true)
                    $currentFileHash = (Get-FileHash -Algorithm $checksumAlgorithm -Path $downloadedFile).Hash
                    if ($currentFileHash -ne $expectedFileHash) {
                        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] File name 's3://$bucketName/$bucketPrefix/$fileName' has an incorrect checksum. Unable to update the certificate $certNamespace at path '$certPath'."
                        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Current file hash: $currentFileHash, Expected file hash: $expectedFileHash."
                        break
                    }
                }
            }
        }
        finally {
            if ($PSCmdlet.ShouldProcess($CertPath, "Change location back to the original location")) {
                Pop-Location
            }
        }
    }
    $ErrorActionPreference = "Stop"
    try {
        Show-Banner -AsLog -Message "Start -> Dotmatics - *.dotmatics.net Certificate Sync"
        $dtxDefaults = Get-Defaults
        $certNamespace = "star.dotmatics.net"
        $certCurrentVersion = $dtxDefaults.Certificates[$certNamespace].CurrentVersion
        $certMetadata = $dtxDefaults.Certificates[$certNamespace].Versions."v$certCurrentVersion"
        $certPath = Join-Path ([DTXPathProvider]::GetLocalDotmaticsSharedCertPath()) $certNamespace
        if (!(Test-ChangeRequired -CertPath $certPath -CertMetadata $certMetadata)) {
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] No change required. Exiting..."
            Set-StateItem -Key "certificates-[$certNamespace]-fingerprint-sha1" -Value $certMetadata.Fingerprint.SHA1
            Set-StateItem -Key "certificates-[$certNamespace]-path" -Value $certPath
            Set-StateItem -Key "certificates-[$certNamespace]-version" -Value $certCurrentVersion
            return
        }
        New-CertPath -CertPath $certPath
        Get-CertFilesFromS3 -CertPath $certPath -CertMetadata $certMetadata
        if ($PSCmdlet.ShouldProcess($certPath, "Test if the update was successful")) {
            if (!(Test-ChangeRequired -CertPath $certPath -CertMetadata $certMetadata)) {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Certificate $certNamespace updated successfully at path '$certPath'."
                Set-StateItem -Key "certificates-[$certNamespace]-fingerprint-sha1" -Value $certMetadata.Fingerprint.SHA1
                Set-StateItem -Key "certificates-[$certNamespace]-path" -Value $certPath
                Set-StateItem -Key "certificates-[$certNamespace]-version" -Value $certCurrentVersion
                Set-StateItem -Key "certificates-[$certNamespace]-install-date" -Value (Get-Date -Format "yyyy-MM-dd")
            }
            else {
                Write-LogCritical -ThrowException -Message "[$( $MyInvocation.MyCommand )] Certificate $certNamespace update failed at path '$certPath'."
            }
        }
    }
    finally {
        Show-Banner -AsLog -Message "Stop -> Dotmatics - *.dotmatics.net Certificate Sync"
    }
}
function Test-CanOverWriteFile {
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$FilePath
    )
    if (Test-Path -Path $FilePath) {
        $FileStream = [System.IO.File]::Open($FilePath,'Open','Write')
        $FileStream.Close()
        $FileStream.Dispose()
    }
}
function Test-HTTPService {
    param(
        [Parameter(Mandatory)]
        [string] 
        $Uri,
        [ValidateRange(100, 599)]
        [int] 
        $ExpectedStatusCode,
        [ValidatePattern("[1-5][0-9]{2}-[1-5][0-9]{2}")]
        [string] 
        $ExpectedStatusCodeRange,
        [int]$WaitTimeInSeconds = 3,
        [int]$MaxRetryCount = 20,
        [switch] $SkipCertificateCheck
    )
    process {
        return [DTXTestUtils]::HTTPService($Uri, $ExpectedStatusCode, $ExpectedStatusCodeRange, $WaitTimeInSeconds, $MaxRetryCount, $SkipCertificateCheck)
    }
}
function Test-IsAppInstalled {
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $SearchTerm
    )
    if (-not $IsWindows) {
        Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] This cmdlet is only supported on Windows." -ThrowException
    }
    $isInstalled = Get-InstalledApps | Where-Object { $_.DisplayName -like $SearchTerm }
    if ($isInstalled.Count -gt 1) {
        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Multiple applications were found with the search term '$SearchTerm'."
        foreach ($app in $isInstalled) {
            Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Application name: $($app.DisplayName)"
        }
        return $true
    }
    if ($isInstalled) {
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] The application name matching search term '$SearchTerm' is installed. The application name is '$($isInstalled.DisplayName)'."
        return $true
    }
    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] The application name matching search term '$SearchTerm' is not installed."
    return $false
}
function Test-IsBrowserSystem {
    try {
        $tomcatHomePath = Get-TomcatHomePath
    }
    catch {
        return $false
    }
    return [DTXBrowser]::new((Get-TomcatWebAppPath)).IsBrowserSystem()
}
function Test-IsServiceRunning {
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $SearchTerm
    )
    if (-not $IsWindows) {
        Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] This cmdlet is only supported on Windows." -ThrowException
    }
    $service = Get-Service -Name $SearchTerm -ErrorAction SilentlyContinue
    if (-not $service) {
        Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] No service with the name '$SearchTerm' was found." -ThrowException
    }
    if ($service.Count -gt 1) {
        Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] More than one service with the name '$SearchTerm' was found. Please specify a more specific search term." -ThrowException
    }
    if ($service.Status -eq "Running") {
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] The service '$($service.Name)' is running."
        return $true
    }
    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] The service '$($service.Name)' is not running. Current status is '$($service.Status)'."
    return $false
}
function Test-IsValidXml {
    param(
        [Parameter(Mandatory = $true)]
        [string]$XmlString
    )
    return [DTXXmlUtils]::TestXmlContentIsValid($XmlString)
}
function Test-Mol2NameBatchOutput {
    param(
        [ValidateScript({ Test-Path $_ -PathType Container })]
        [string] $WorkingDirectory
    )
    try {
        Push-Location -Path $WorkingDirectory
        $outputFile = "auto_output.txt"
        $sampleOutputFile = "sample_test_out.txt"
        $sampleDataFile = "batch.sd"
        if (Test-Path $outputFile) {
            Remove-Item -Path $outputFile -Force
        }
        if ($IsWindows) {
            & (Join-Path $pwd "mol2name_batch.exe") $sampleDataFile $outputFile
            if (!(Test-Path $outputFile)) {
                Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] mol2name_batch.exe is not able to genereate a sample output using the sample data file." -ThrowException 
            }
        }
        if ($IsLinux) {
            & (Join-Path $pwd "mol2name_batch") $sampleDataFile $outputFile
            if (!(Test-Path $outputFile)) {
                Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] mol2name_batch is not able to genereate a sample output using the sample data file." -ThrowException 
            }
        }
        if ((Get-Content -Path $outputFile -Raw) -ne (Get-Content -Path $sampleOutputFile -Raw)) {
            return $false
        }
        return $true
    }
    finally {
        Remove-Item -Path $outputFile -Force -ErrorAction SilentlyContinue
        Pop-Location
    }
}
function Test-OpenEyeIntegrity {
    param (
        [string] $Version,
        [string] $TargetPath
    )
    try {
        Write-LogInfo -Message "[$($MyInvocation.MyCommand)] Testing Open Eye file integrity... "
        $openEyeDefaults = Get-OpenEyeDefaults -Version $Version
        $expectedFiles = $openEyeDefaults.FileChecksums.Files
        $expectedFilesChecksumAlgorithm = $openEyeDefaults.FileChecksums.Algorithm
        $targetFiles = Get-ChildItem -Path $TargetPath
        foreach ($expectedFile in $expectedFiles.Keys) {
            $expectedFileName = $expectedFile
            $expectedFileHash = $expectedFiles[$expectedFile]
            if ($targetFiles.Name -notcontains $expectedFileName) {
                Write-LogWarning -Message "[$($MyInvocation.MyCommand)] Open Eye file integrity test failed due to missing files..."
                Write-LogWarning -Message "[$($MyInvocation.MyCommand)] Expected file: $expectedFileName is not found in the Open Eye directory."
                return $false
            }
            $targetFile = $targetFiles | Where-Object { $_.Name -eq $expectedFileName }
            $targetFileHash = (Get-FileHash -Path $targetFile.PSPath -Algorithm $expectedFilesChecksumAlgorithm).Hash.ToLower()
            if ($expectedFileHash -ne $targetFileHash) {
                Write-LogWarning -Message "[$($MyInvocation.MyCommand)] Open Eye file integrity test failed due to checksum mismatch..."
                Write-LogWarning -Message "[$($MyInvocation.MyCommand)] $($targetFile.Name) file hash is not matching the expected file hash of $expectedFileHash."
                return $false
            }
        }
        return $true
    }
    finally {
        Write-LogInfo -Message "[$($MyInvocation.MyCommand)] Open Eye file integrity test complete."
    }
}
function Test-OpenEyeIsUpToDate {
    param (
        [string] $Version
    )
    if (!$IsWindows -and !$IsLinux) {
        Write-LogCritical -ThrowException "[$( $MyInvocation.MyCommand )] This cmdlet is only supported on Windows or Linux operating systems."
    }
    Write-LogInfo -Message "[$($MyInvocation.MyCommand)] Checking if Open Eye is installed and up to date..."
    $openEyeDefaults = Get-OpenEyeDefaults -Version $Version
    $gifCacheToolkitPath = Get-BrowserPlatformGifcacheToolkitPath
    $checksumAlgorithm = $openEyeDefaults.FileChecksums.Algorithm
    $fileName = $null
    if ($IsWindows) {
        $fileName = "mol2name_batch.exe"
    }
    if ($IsLinux) {
        $fileName = "mol2name_batch"
    }
    $filePath = (Join-Path $gifCacheToolkitPath $fileName)
    if (!(Test-Path -Path $filePath)) {
        Write-LogInfo -Message "[$($MyInvocation.MyCommand)] Open Eye is not installed on this machine."
        return $false
    }
    $actualFileHash = (Get-FileHash -Path $filePath -Algorithm $checksumAlgorithm).Hash.ToLower()
    $expectedFileHash = ($openEyeDefaults.FileChecksums.Files.$fileName).ToLower()
    if ($actualFileHash -ne $expectedFileHash) {
        Write-LogInfo -Message "[$($MyInvocation.MyCommand)] Open Eye is not up to date on this machine."
        return $false
    }
    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Open Eye is up to date on this machine."
    return $true
}
function Test-OracleConnectionString {
    [cmdletbinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $ConnectionString,
        [switch]$NoCache
    )
    function Invoke-Query($connectionString, $SQLQuery) {
        Write-Debug "Performing query: $SQLQuery"
        $returnObj = New-Object Collections.Generic.List[HashTable]
        $connection2 = New-Object Oracle.ManagedDataAccess.Client.OracleConnection($connectionString)
        try {
            $connection2.Open()
            Write-Debug "Connection to Oracle database established."
            $command = $connection2.CreateCommand()
            $command.CommandText = $sqlQuery
            $reader = $command.ExecuteReader()
            Write-Debug "Output:"
            while ($reader.Read()) {
                $myoutput = @{}
                for ($i = 0; $i -lt $reader.FieldCount; $i++) {
                    $myoutput.Add($reader.GetName($i), $reader.GetValue($i))
                    Write-Debug ($reader.GetName($i) + ": " + $reader.GetValue($i))
                }
                $returnObj.Add($myoutput)
            }
            $reader.Close()
            return  $returnObj
        }
        catch {
            Write-Debug "An error occurred: $_"
            throw
        }
        finally {
            $connection2.Close()
        }
    }
    function _Test-OracleConnectionString {
        [cmdletbinding()]
        param (
            [Parameter(Mandatory = $true)]
            [string]
            $ConnectionString
        )
        Write-Debug "Testing connection string"
        try {
            Invoke-Query $ConnectionString "select * from dual"
            Write-Debug "Testing connection string was successful. Result is true"
            return $true
        }
        catch {
            Write-Debug "Testing connection string was unsuccessful. Result is false"       
            return $false
        }
    }
    $hash = get-StringHash $ConnectionString
    if ($NoCache) {
        $global:TestDBConnectionCache = @{created = Get-Date } 
        return _Test-OracleConnectionString $ConnectionString
    } 
    if (-not (Get-Variable -Name TestDBConnectionCache  -Scope Global -ErrorAction SilentlyContinue)) {
        $global:TestDBConnectionCache = @{created = Get-Date } 
    }
    if ($global:TestDBConnectionCache.ContainsKey("created") -and ((Get-Date) -gt ($global:TestDBConnectionCache["created"]).AddHours(1))) {
        $global:TestDBConnectionCache = @{created = Get-Date } 
        Write-Debug "Cache expired. Invalidating."
        $res = _Test-OracleConnectionString $ConnectionString
        Write-Debug "Adding $hash to cache. Value $res."
        $global:TestDBConnectionCache[$hash] = $res
        return $res
    }
    if ($global:TestDBConnectionCache.ContainsKey($hash)) {
        Write-Debug "Value $($global:TestDBConnectionCache[$hash]) from from cache. Key is $hash"
        return $global:TestDBConnectionCache[$hash]
    }
    else {
        $res = _Test-OracleConnectionString $ConnectionString
        $global:TestDBConnectionCache[$hash] = $res
        Write-Debug "Adding $hash to cache. Value $res."
        return $res
    }
}
function Test-SSMCommandResult {
    param (
        [Parameter(Mandatory = $true)]
        [Amazon.SimpleSystemsManagement.Model.Command]
        $Result,
        [Parameter(Mandatory = $true)]
        [String]
        $InstanceId,
        [Parameter(Mandatory = $true)]
        [String]
        $Region
    )
    process {
        if (-not $Result.CommandId) {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] The command ID is missing." -ThrowException
        }
        $invokeResult = Get-SSMCommandInvocationDetail -InstanceId $InstanceId -CommandId $Result.CommandId -Region $Region
        if ($invokeResult.Status -ne "Success") { Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] The operation failed." -ThrowException }
        $stdOutResult = $invokeResult.StandardOutputContent | ConvertFrom-Json
        if ($stdOutResult.Status -ne "OK") {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] $($stdOutResult.Message)" -ThrowException
        }
        return $true
    }
}
function Test-SSMCommandResultV2 {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $CommandId,
        [Parameter(Mandatory = $true)]
        [string]
        $InstanceId,
        [Parameter(Mandatory = $true)]
        [string]
        $Region,
        [switch]
        $Wait,
        [switch]
        $ThrowException
    )
    process {
        if ($Wait) {
            Wait-ForSSMCommand -CommandId $CommandId -InstanceId $InstanceId -Region $Region
        }
        $commandResult = Get-SSMCommandInvocationDetail -InstanceId $InstanceId -CommandId $CommandId -Region $Region
        if (-not $commandResult) {
            $message = "[$($MyInvocation.MyCommand)] No SSM command invocation result found for command id '$CommandId' in region '$Region'."
            Write-LogCritical -Message $message -ThrowException
        }
        if ($commandResult.Status -ne "Success") {            
            if ($ThrowException) {
                Write-LogError -Message "[$($MyInvocation.MyCommand)] Failed SSM command '$CommandId' with doc '$($commandResult.DocumentName)' in region '$Region'."
                Write-LogError -Message "[$($MyInvocation.MyCommand)] Command status is '$($commandResult.Status)'."
                Write-LogError -Message "[$($MyInvocation.MyCommand)] More info: https://$($Region).console.aws.amazon.com/systems-manager/run-command/$($CommandId)?region=$($Region)"
                Write-SSMCommandOutput -InstanceId $InstanceId -CommandId $CommandId -Region $Region -AsError:$true
                Write-LogCritical -Message "[$($MyInvocation.MyCommand)] Failed SSM command '$CommandId' with doc '$($commandResult.DocumentName)' in region '$Region'." -ThrowException
            }
            return $false
        }
        return $true
    }
}
function Test-SSMReachability {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [String]
        $InstanceId,
        [Parameter(Mandatory = $true)]
        [String]
        $Region,
        [Parameter(Mandatory = $false)]
        [int]
        $MaxRegistrationAttempts = 60,
        [Parameter(Mandatory = $false)]
        [int]
        $MaxExecutionAttempts = 40,
        [Parameter(Mandatory = $false)]
        [int]
        $SleepDuration = 3,
        [switch]
        $SkipCommandExecution,
        [switch]
        $ThrowException
    )
    function Test-InstanceRegisteredToSSM {
        [CmdletBinding()]
        param (
            [string]$InstanceId,
            [string]$Region
        )
        $filter = @{
            Key    = "PingStatus"
            Values = "Online"
        }
        $ssmOnlineInstances = Get-SSMInstanceInformation -Filter $filter -Region $Region
        return $ssmOnlineInstances.InstanceId -contains $InstanceId.ToLower()
    }
    function Invoke-SSMCommandAndCheckStatus {
        param (
            [string]$InstanceId,
            [string]$Region,
            [int]$MaxAttempts,
            [int]$SleepDuration
        )
        $command = Send-SSMCommand -DocumentName "AWS-RunPowerShellScript" -Parameter @{ commands = "Get-Date" } -Region $region -Target @{ Key = "instanceids"; Values = @($InstanceId) }
        for ($i = 0; $i -le $MaxAttempts; $i++) {
            $commandResult = Get-SSMCommandInvocation -CommandId $command.CommandId -Region $Region -Detail $true
            if ($commandResult.Status.Value -match "Success") {
                return $true
            }
            Start-Sleep -Seconds $SleepDuration
        }
        return $false
    }
    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Checking SSM registration status for instance '$InstanceId'. Please wait..."
    $isRegistered = $false
    for ($i = 0; $i -le $MaxRegistrationAttempts; $i++) {
        if (Test-InstanceRegisteredToSSM -instanceId $InstanceId -region $Region) {
            $isRegistered = $true
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Instance '$InstanceId' is registered to SSM."
            break
        }
        Start-Sleep -Seconds $SleepDuration
    }
    if (-not $isRegistered) {
        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Instance '$InstanceId' is not registered to SSM."
        if ($TrhrowException) {
            Write-LogCritical -Message "[$($MyInvocation.MyCommand)] Instance '$InstanceId' is not registered to SSM." -ThrowException
        }
        return $false
    }
    if (-not $SkipCommandExecution) {
        $canExecute = Invoke-SSMCommandAndCheckStatus -instanceId $InstanceId -region $Region -maxAttempts $MaxExecutionAttempts -sleepDuration $SleepDuration
        if (-not $canExecute) {
            Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Instance '$InstanceId' is not reachable via SSM."
            if ($TrhrowException) {
                Write-LogCritical -Message "[$($MyInvocation.MyCommand)] Instance '$InstanceId' is not reachable via SSM." -ThrowException
            }
            return $false
        }
    }
    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Instance '$InstanceId' is reachable via SSM."
    return $true
}
function Uninstall-SumoLogic {
    param (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]
        $ServiceName,
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]
        $uninstallFile
    )
    $ErrorActionPreference = 'Stop'
    $ProgressPreference = 'SilentlyContinue'
    $WarningPreference = 'SilentlyContinue'
    If (-not (Test-Path $uninstallFile)) {
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] $uninstallFile does not exist , uninstall skipped."
        return
    }
    $dtxSystem=Get-DTXSystem
    $instanceId = $dtxSystem.GenericInfo.IdentityInfo.instanceId
    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Executing $uninstallFile for instance $instanceId ..."
    Start-Process $uninstallFile -Wait -ArgumentList "-q","-console"
    $WaitAfterSeconds=10
    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Waiting $WaitAfterSeconds seconds for service to deregister ..."
    Start-Sleep -Seconds $WaitAfterSeconds
    Assert-True -Condition (-not (Get-Service -ErrorAction SilentlyContinue | Where-Object { $_.Name -eq $ServiceName})) -message "[$( $MyInvocation.MyCommand )] $ServiceName should not exist at this point after uninstall process" 
    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] SumoLogic agent uninstalled."
}
function Update-XmlAttribute {
    param (
        [System.Xml.XmlElement]
        $Element,
        [string]
        $Attribute,
        [string]
        $Value
    )
    if ($element -and $element.HasAttribute($attribute)) {
        $element.SetAttribute($attribute, $value)
    }
}
function Using-Object
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [AllowEmptyCollection()]
        [AllowNull()]
        [Object]
        $InputObject,
        [Parameter(Mandatory = $true)]
        [scriptblock]
        $ScriptBlock
    )
    try
    {
        . $ScriptBlock
    }
    finally
    {
        if ($null -ne $InputObject -and $InputObject -is [System.IDisposable])
        {
            $InputObject.Dispose()
        }
    }
}
function Wait-ForSSMAssociation {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]$InstanceId,
        [Parameter(Mandatory = $true)]
        [string]$Region
    )
    begin {
        $MAX_ATTEMPTS = 3
        $SLEEP_DURATION = 5
    }
    process {
        $isInstanceRegisteredToSSM = Test-SSMReachability -InstanceId $InstanceId -Region $Region -SkipCommandExecution
        if ($isInstanceRegisteredToSSM) {
            $count = 0
            while ($count -le $MAX_ATTEMPTS) {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Checking SSM association status for instance '$InstanceId'..."
                $filter = @{
                    Key   = "Status"
                    Value = "InProgress"
                }
                $result = Get-SSMCommand -InstanceId $InstanceId -Region $Region -Filter $filter
                if ($result.count -ne 0) {
                    $count = 0
                    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] $($result.count) SSM association(s) in progress for instance '$InstanceId'."
                }
                else {
                    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] No SSM association in progress for instance '$InstanceId'."
                    $time_left = ($MAX_ATTEMPTS - $count) * $SLEEP_DURATION
                    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Time left: $time_left seconds"
                    $count++  
                }
                Start-Sleep -Seconds $SLEEP_DURATION
            }
            return $true  
        }
        else {
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Instance not registered in SSM. Skipping SSM association check."
            return $false  
        }
    }
}
function Wait-ForSSMCommand {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $CommandId,
        [Parameter(Mandatory = $true)]
        [string]
        $InstanceId,
        [Parameter(Mandatory = $true)]
        [String]
        $Region
    )
    begin {
        $MAX_ATTEMPTS = 720 
        $SLEEP_DURATION = 5
        function Get-CommandResult {
            try {
                return (Get-SSMCommandInvocationDetail -CommandId $CommandId -InstanceId $InstanceId  -Region $Region)
            }
            catch {
                if ($_ -like "*InvocationDoesNotExist*") {
                    return $null
                }
                else {
                    Write-LogCritical -Message "[$($MyInvocation.MyCommand)] Get-CommandResult Failed" -ThrowException -Exception $_
                }
            }
        }
        function Test-IsCommandComplete {
            $res = Get-CommandResult
            if (($null -eq $res) -or ($null -eq $res.ExecutionEndDateTime) -or ($res.ExecutionEndDateTime -eq '')) {
                if ($null -eq $res.Status -or $res.Status -eq '' -or $res.Status -eq 'Pending' -or $res.Status -eq 'InProgress') {
                    return $false
                }
            }
            return $true
        }
    }
    process {
        try {
            $attempts = 0
            $isComplete = Test-IsCommandComplete
            while (-not $isComplete -and $attempts -lt $MAX_ATTEMPTS) {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Waiting for SSM command '$CommandId' to complete."
                Start-Sleep -Seconds $SLEEP_DURATION
                $isComplete = Test-IsCommandComplete
                $attempts++
            }
            if ($attempts -ge $MAX_ATTEMPTS) {
                Write-LogCritical -Message "[$($MyInvocation.MyCommand)] Max attempts reached. SSM command '$CommandId' did not complete within the time limit." -ThrowException
            }
        }
        catch {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] Exception caught: Unable to retrieve the status of SSM command '$CommandId'." -ThrowException -Exception $_
        }
    }
}
function Wait-ForSSMReachability {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $InstanceId,
        [Parameter(Mandatory = $true)]
        [String]
        $Region
    )
    process {
        Test-SSMReachability -InstanceId $InstanceId -Region $Region -MaxRegistrationAttempts 12 -SleepDuration 10
    }
}
function Wait-ForWebServerToServeRequests {
    param (
        [ValidateNotNullOrEmpty()]
        [string]$Hostname = 'localhost',
        [ValidateNotNullOrEmpty()]
        [int]$Port = 443,
        [ValidateSet('http', 'https')]
        [string]$Scheme = 'https',
        [ValidateNotNullOrEmpty()]
        [int]$WaitSeconds = 300,
        [switch]$SkipCertificateCheck
    )
    $webServerUrl = "$($Scheme)://$($Hostname):$($Port)"
    if (($Port -eq 443) -and ($Scheme -eq 'https')) {
        $webServerUrl = "$($Scheme)://$($Hostname)"
    }
    $startTime = Get-Date
    $endTime = $startTime.AddSeconds($WaitSeconds)
    $isLive = $false
    $atLeastOneRetry = $false
    while ((Get-Date) -lt $endTime) {
        try {
            $response = Invoke-WebRequest -Uri $webServerUrl -UseBasicParsing -Method Head -TimeoutSec 1 -SkipCertificateCheck:$SkipCertificateCheck
            if ($response.StatusCode -gt 199 -and $response.StatusCode -lt 399) {
                Write-LogInfo -Message "[$($MyInvocation.InvocationName)] The web server on $webServerUrl is serving requests."
                $isLive = $true
                break
            }
        }
        catch [System.Net.Http.HttpRequestException] {
            if ($_.Exception.InnerException -and $_.Exception.InnerException.Message -like "*RemoteCertificateNameMismatch*") {
                Write-LogError -Message "[$($MyInvocation.InvocationName)] Unable to connect to $webServerUrl. The certificate does not match the expected hostname. Use -SkipCertificateCheck to bypass this check."
                throw $_
            }
        }
        catch {
            $atLeastOneRetry = $true
        }
        Start-Sleep -Seconds 1
        $timeLeft = [math]::Truncate(($endTime - (Get-Date)).TotalSeconds)
        Write-LogInfo -Message "[$($MyInvocation.InvocationName)] Waiting for the web server on $webServerUrl to start serving requests. Time left: $timeLeft seconds"
    }
    if (-not $isLive) {
        Write-LogWarning -Message "[$($MyInvocation.InvocationName)] The web server on $webServerUrl did not start serving requests within the specified time."
    }
    if ($isLive -and $atLeastOneRetry) {
        $totalWaitTime = [math]::Truncate(((Get-Date) - $startTime).TotalSeconds)
        Write-LogInfo -Message "[$($MyInvocation.InvocationName)] It took $totalWaitTime seconds for the web server on $webServerUrl to start serving requests."
    }
}
function Write-SSMCommandOutput {
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $CommandId,
        [Parameter(Mandatory = $true)]
        [string]
        $InstanceId,
        [Parameter(Mandatory = $true)]
        [string]
        $Region,
        [switch]
        $AsError
    )
    process {
        $commandStepsOutput = Get-SSMCommandOutput -CommandId $CommandId -InstanceId $InstanceId -Region $Region
        foreach ($step in $commandStepsOutput.GetEnumerator()) {
            $stdOut = $step.Value.StandardOutput
            $stdErr = $step.Value.StandardError
            if (-not $stdOut -and -not $stdErr) {
                Write-LogInfo -Message "[$($MyInvocation.MyCommand)] No standard output or standard error content for command id '$CommandId' with step name '$($step.Name)' in region '$Region'."
                return
            }
            if ($AsError) {
                Write-LogError -Message "[$($MyInvocation.MyCommand)] === Output for CommandId: $($CommandId) ==="
                if ($stdOut) {
                    Write-LogError -Message  "[$($MyInvocation.MyCommand)] === BEGIN Standard Output ==="
                    Write-LogError -Message  $stdOut
                    Write-LogError -Message  "[$($MyInvocation.MyCommand)] === END Standard Output ==="
                }
                if ($stdErr) {
                    Write-LogError -Message "[$($MyInvocation.MyCommand)] === BEGIN Standard Error ==="
                    Write-LogError -Message $stdErr
                    Write-LogError -Message "[$($MyInvocation.MyCommand)] === END Standard Error ==="
                }
                continue
            }
            Write-LogInfo -Message "[$($MyInvocation.MyCommand)] === Output for CommandId: $($CommandId) ==="
            if ($stdOut) {
                Write-LogInfo -Message "[$($MyInvocation.MyCommand)] === BEGIN Standard Output ==="
                Write-LogInfo -Message $stdOut
                Write-LogInfo -Message "[$($MyInvocation.MyCommand)] === END Standard Output ==="
            }
            if ($stdErr) {
                Write-LogWarning -Message "[$($MyInvocation.MyCommand)] === BEGIN Standard Error ==="
                Write-LogWarning -Message $stdErr
                Write-LogWarning -Message "[$($MyInvocation.MyCommand)] === END Standard Error ==="
            }
        }
    }
}
function Write-StringToFileWithRetry {
    param(
        [Parameter(Mandatory = $true)]
        [string]$StringContent,
        [Parameter(Mandatory = $true)]
        [string]$FilePath,
        [Int]$RetryCount = 3,
        [Int]$WaitSeconds = 2
    )
    $retryAttempts = 0
    do {
        try {
            $StringContent | Out-File -Force -FilePath $FilePath
            break
        }
        catch {
            if ($retryAttempts -ge $RetryCount) {
                Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] Failed to write to file '$FilePath' after $($RetryCount + 1) attempts." -ThrowException 
            }
            Start-Sleep -Seconds $WaitSeconds
            $retryAttempts++
        }
    } while ($retryAttempts -le $RetryCount)
}
function Assert-CorrectEC2Tag {
  param (
    [string] $Key,
    [array]$AllowedValues
  )
  $myInfo = Get-DTXSystem
  $instanceId = $myInfo.GenericInfo.IdentityInfo.instanceId
  Assert-True -Condition ($instanceId -ne $null) -message "[$( $MyInvocation.MyCommand )] Instance ID was null"
  Assert-True -Condition ($instanceId.Length -gt 0) -message "[$( $MyInvocation.MyCommand )] Instance ID was length 0"
  $allTags = Get-EC2Tags -InstanceId $instanceId
  $myValue = Get-TagValue -Key $Key -Tags $allTags
  if ($AllowedValues.Contains($myValue)) {
    Write-LogInfo "[$( $MyInvocation.MyCommand )] Tag value is in the allowed values list, tag value confirmed"
  }
  else {
    Write-LogWarning "[$( $MyInvocation.MyCommand )] Tag value($myValue) is NOT in allowed values: TagKey: $Key TagAllowedValues: $AllowedValues" 
    throw (New-Object DTXPSException("[$( $MyInvocation.MyCommand )] Incorrect Tag Value"))
  }
}
function Format-AutomationParams {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [ValidateSet('True', 'False', IgnoreCase = $true)]
        [string]
        $DryRun,
        [Parameter(Mandatory)]
        [ValidateSet('True', 'False', IgnoreCase = $true)]
        [string]
        $RebootRequested,
        [Parameter(Mandatory)]
        [ValidateSet('True', 'False', IgnoreCase = $true)]
        [string]
        $InMaintWindow,
        [Parameter(Mandatory)]
        [ValidateScript({ $_ -as [int] })]
        [string]
        $RebootExitCode,
        [Parameter(Mandatory)]
        [ValidateScript({ $_ -as [int] })]
        [string]
        $ContinueOnErrorExitCode,
        [Parameter(Mandatory)]
        [string]
        $EC2Role,
        [switch]
        $AsHashtable
    )
    $result = [PSCustomObject]@{
        DryRun                  = [bool]::Parse($DryRun)
        RebootRequested         = [bool]::Parse($RebootRequested)
        InMaintWindow           = [bool]::Parse($InMaintWindow)
        RebootExitCode          = [int]::Parse($RebootExitCode)
        ContinueOnErrorExitCode = [int]::Parse($ContinueOnErrorExitCode)
        EC2Role                 = $EC2Role
    }
    if ($AsHashtable) {
        return @{
            DryRun                  = $result.DryRun
            RebootRequested         = $result.RebootRequested
            InMaintWindow           = $result.InMaintWindow
            RebootExitCode          = $result.RebootExitCode
            ContinueOnErrorExitCode = $result.ContinueOnErrorExitCode
            EC2Role                 = $result.EC2Role
        }
    }
    return $result
}
function Get-DefaultsT {
  return Get-Defaults
}
function Get-DTXLocalDiscoveryFilePath {
    return [DTXPathProvider]::GetDiscoveryFilePath()
}
function Get-DTXMyInvocationDetails {
    [CmdletBinding()]
    param()
    return $PSCmdlet.MyInvocation
}
function Get-DTXSystem {
    param(
        [switch]$NoCache
    )
    function Get-InternalDTXSystem {
        $returnObj = @{
            GenericInfo     = @{
                DiskInfo     = New-Object Collections.Generic.List[PSObject]
                IdentityInfo = @{}
                InstanceTags = "UNKNOWN"
                HostName     = "UNKNOWN"
                PublicIPv4   = "UNKNOWN"
                IAMProfile   = "UNKNOWN"
            }
            AppInfo         = @{
                Tomcat          = @{
                    Running     = "UNKNOWN"
                    Service     = @{
                        Name              = "UNKNOWN"
                        Status            = "UNKNOWN"
                        StartType         = "UNKNOWN"
                        DependentServices = "UNKNOWN"
                        DependsOnServices = "UNKNOWN"
                        User              = "UNKNOWN"
                    }
                    Path        = "UNKNOWN"
                    WebAppsPath = "UNKNOWN"
                    Version     = "UNKNOWN"
                    Java        = @{
                        Version = "UNKNOWN"
                        Vendor  = "UNKNOWN"
                        Path    = "UNKNOWN"
                    }
                    ServerXML   = @{
                        Path         = "UNKNOWN"
                        Port         = "UNKNOWN"
                        HttpProtocol = "UNKNOWN"
                        Http2Enabled = "UNKNOWN"
                        SSLProtocols = "UNKNOWN"
                        SSLCiphers   = "UNKNOWN"
                        KeyStoreFile = "UNKNOWN"
                        WebAppBase   = "UNKNOWN"
                    }
                    Certificate = @{
                        Fingerprint = @{
                            SHA1 = "UNKNOWN"
                        }
                    }
                    Options     = @{
                        JavaOpts              = "UNKNOWN"
                        JVM                   = "UNKNOWN"
                        JavaMinMemoryMB       = -1
                        JavaMaxMemoryMB       = -1
                        JavaThreadStackSizeKB = -1
                    }
                }
                Oracle          = @{
                    Database         = @{
                        Running = "UNKNOWN"
                        Service = @{
                            Name              = "UNKNOWN"
                            Status            = "UNKNOWN"
                            StartType         = "UNKNOWN"
                            DependentServices = "UNKNOWN"
                            DependsOnServices = "UNKNOWN"
                        }
                        Path    = "UNKNOWN"
                        Version = "UNKNOWN"
                    }
                    DatabaseListener = @{
                        Running = "UNKNOWN"
                        Service = @{
                            Name              = "UNKNOWN"
                            Status            = "UNKNOWN"
                            StartType         = "UNKNOWN"
                            DependentServices = "UNKNOWN"
                            DependsOnServices = "UNKNOWN"
                        }
                        Path    = "UNKNOWN"
                        Version = "UNKNOWN"
                    }
                }
                JChem           = @{
                    Cartridge = @{
                        Running = "UNKNOWN"
                        Service = @{
                            Name              = "UNKNOWN"
                            Status            = "UNKNOWN"
                            StartType         = "UNKNOWN"
                            DependentServices = "UNKNOWN"
                            DependsOnServices = "UNKNOWN"
                        }
                        Path    = "UNKNOWN"
                        Version = "UNKNOWN"
                    }
                }
                BrowserPlatform = @{
                    IsBrowserSystem = "UNKNOWN"
                    PropertiesPath  = "UNKNOWN"
                    Properties      = @{}
                    Plugins         = @{
                        OpenEye = @{
                            UserFriendlyVersion   = "UNKNOWN"
                            Mol2NameChecksum      = "UNKNOWN"
                            Mol2NameBatchChecksum = "UNKNOWN"
                        }
                    }
                }
            }
            StateInfo       = New-Object System.Collections.Hashtable
            ComputedQueries = @{
                IsBrowserPlatformSystem = @{
                    Status = "UNKNOWN"
                }
                IsWebServer             = @{
                    Status  = "UNKNOWN"
                    AppName = "UNKNOWN"
                }
                IsDBServer              = @{
                    Status  = "UNKNOWN"
                    AppName = "UNKNOWN"
                }
            }
        }
        function Get-BasicVMInfo {
            $returnObj.GenericInfo.HostName = $env:COMPUTERNAME
            $returnObj.GenericInfo.DiskInfo = Get-DiskInformation
            $returnObj.GenericInfo.IdentityInfo = (Get-EC2InstanceMetadata -Category "IdentityDocument" -ErrorAction SilentlyContinue | ConvertFrom-Json -ErrorAction SilentlyContinue) ?? $returnObj.GenericInfo.IdentityInfo
            $returnObj.GenericInfo.IAMProfile = (Get-EC2InstanceMetadata -Path "/iam/info" -ErrorAction SilentlyContinue | ConvertFrom-Json -ErrorAction SilentlyContinue).InstanceProfileArn ?? $returnObj.GenericInfo.IAMProfile
            $returnObj.GenericInfo.PublicIPv4 = (Get-EC2InstanceMetadata -Category "PublicIpv4" -ErrorAction SilentlyContinue) ?? $returnObj.GenericInfo.PublicIPv4
        }
        function Get-TomcatAppInfo {
            $returnObj.AppInfo.Tomcat.Running = [bool](Get-TomcatProcess)
            if ($returnObj.AppInfo.Tomcat.Running) {
                $meta = Get-TomcatMetadata
                $returnObj.AppInfo.Tomcat.Service.Name = $meta.TomcatService.Name ?? $returnObj.AppInfo.Tomcat.Service.Name
                $returnObj.AppInfo.Tomcat.Service.Status = $meta.TomcatService.Status ?? $returnObj.AppInfo.Tomcat.Service.Status
                $returnObj.AppInfo.Tomcat.Service.StartType = (($meta.TomcatService.StartType ?? $meta.TomcatService.StartMode) | Out-String -NoNewline) ?? $returnObj.AppInfo.Tomcat.Service.StartType
                $returnObj.AppInfo.Tomcat.Service.User = $meta.TomcatServiceUser ?? $returnObj.AppInfo.Tomcat.Service.User
                if ($meta.TomcatService.DependentServices.Count -eq 0) {
                    $returnObj.AppInfo.Tomcat.Service.DependentServices = $meta.TomcatService.DependentServices
                }
                else {
                    $returnObj.AppInfo.Tomcat.Service.DependentServices = $meta.TomcatService.DependentServices | Select-Object Name, DisplayName, Status
                }
                if ($meta.TomcatService.ServicesDependedOn.Count -eq 0) {
                    $returnObj.AppInfo.Tomcat.Service.DependsOnServices = $meta.TomcatService.ServicesDependedOn
                }
                else {
                    $returnObj.AppInfo.Tomcat.Service.DependsOnServices = $meta.TomcatService.ServicesDependedOn | Select-Object Name, DisplayName, Status
                }
                $returnObj.AppInfo.Tomcat.Certificate.Fingerprint.SHA1 = $meta.TomcatCertificate.Fingerprint.SHA1 ?? $returnObj.AppInfo.Tomcat.Certificate.Fingerprint.SHA1
                $returnObj.AppInfo.Tomcat.Path = $meta.TomcatHomePath ?? $returnObj.AppInfo.Tomcat.Path
                $returnObj.AppInfo.Tomcat.Version = $meta.TomcatVersion ?? $returnObj.AppInfo.Tomcat.Version
                $returnObj.AppInfo.Tomcat.Java.Version = $meta.TomcatJavaVersion ?? $returnObj.AppInfo.Tomcat.Java.Version
                $returnObj.AppInfo.Tomcat.Java.Vendor = $meta.TomcatJavaVendor ?? $returnObj.AppInfo.Tomcat.Java.Vendor
                $returnObj.AppInfo.Tomcat.Java.Path = $meta.TomcatJavaHomePath ?? $returnObj.AppInfo.Tomcat.Java.Path
                $serverXmlPath = $meta.TomcatServerXmlPath ?? $returnObj.AppInfo.Tomcat.ServerXML.Path
                if (Test-Path $serverXmlPath) {
                    $returnObj.AppInfo.Tomcat.ServerXML.Path = $serverXmlPath
                    try {
                        $xmlFile = [DTXTomcatServerXml]::new($serverXmlPath)
                        $sslConnector = $xmlFile.GetConnectorElementWithSslEnabled()
                        $returnObj.AppInfo.Tomcat.ServerXML.Port = $sslConnector.port ?? $returnObj.AppInfo.Tomcat.ServerXML.Port
                        $returnObj.AppInfo.Tomcat.ServerXML.HttpProtocol = $sslConnector.protocol ?? $returnObj.AppInfo.Tomcat.ServerXML.HttpProtocol
                        $returnObj.AppInfo.Tomcat.ServerXML.Http2Enabled = $xmlFile.IsConnectorHttp2Enabled($sslConnector.port) ?? $returnObj.AppInfo.Tomcat.ServerXML.Http2Enabled
                        $returnObj.AppInfo.Tomcat.ServerXML.SSLProtocols = $sslConnector.sslEnabledProtocols ?? $returnObj.AppInfo.Tomcat.ServerXML.SSLProtocols
                        $returnObj.AppInfo.Tomcat.ServerXML.SSLCiphers = $sslConnector.ciphers ?? $returnObj.AppInfo.Tomcat.ServerXML.SSLCiphers
                        $returnObj.AppInfo.Tomcat.ServerXML.KeyStoreFile = $sslConnector.keystoreFile ?? $returnObj.AppInfo.Tomcat.ServerXML.KeyStoreFile
                        $returnObj.AppInfo.Tomcat.ServerXML.WebAppBase = $xmlFile.GetContents()["Server"]["Service"]["Engine"]["Host"].appBase
                        $returnObj.AppInfo.Tomcat.WebAppsPath = Join-Path $returnObj.AppInfo.Tomcat.Path $returnObj.AppInfo.Tomcat.ServerXML.WebAppBase
                    }
                    catch {
                        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Unable to read the Tomcat server XML file. Please check the file path and permissions."
                    }
                    $returnObj.AppInfo.Tomcat.Options.JavaOpts = $meta.TomcatJavaOptions.Options ?? $returnObj.AppInfo.Tomcat.Options.JavaOpts
                    $returnObj.AppInfo.Tomcat.Options.JVM = $meta.TomcatJavaOptions.JVM ?? $returnObj.AppInfo.Tomcat.Options.JVM
                    $returnObj.AppInfo.Tomcat.Options.JavaMinMemoryMB = $meta.TomcatJavaOptions.JavaMinMemoryMB ?? $returnObj.AppInfo.Tomcat.Options.JavaMinMemoryMB
                    $returnObj.AppInfo.Tomcat.Options.JavaMaxMemoryMB = $meta.TomcatJavaOptions.JavaMaxMemoryMB ?? $returnObj.AppInfo.Tomcat.Options.JavaMaxMemoryMB
                    $returnObj.AppInfo.Tomcat.Options.JavaThreadStackSizeKB = $meta.TomcatJavaOptions.JavaThreadStackSizeKB ?? $returnObj.AppInfo.Tomcat.Options.JavaThreadStackSizeKB
                }
            }
        }
        function Get-JChemAppInfo {
            function Get-JChemCartridgeServiceInfo {
                $returnObj.AppInfo.JChem.Cartridge.Running = [bool](Get-JChemCartridgeProcess)
                if ($returnObj.AppInfo.JChem.Cartridge.Running) {
                    $meta = Get-JChemMetadata
                    $returnObj.AppInfo.JChem.Cartridge.Service.Name = $meta.Cartridge.Service.Name ?? $returnObj.AppInfo.JChem.Cartridge.Service.Name
                    $returnObj.AppInfo.JChem.Cartridge.Service.Status = $meta.Cartridge.Service.Status ?? $returnObj.AppInfo.JChem.Cartridge.Service.Status
                    $returnObj.AppInfo.JChem.Cartridge.Service.StartType = (($meta.Cartridge.Service.StartType ?? $meta.Cartridge.Service.StartMode) | Out-String -NoNewline) ?? $returnObj.AppInfo.JChem.Cartridge.Service.StartType
                    if ($meta.Cartridge.Service.DependentServices.Count -eq 0) {
                        $returnObj.AppInfo.JChem.Cartridge.Service.DependentServices = $meta.Cartridge.Service.DependentServices
                    }
                    else {
                        $returnObj.AppInfo.JChem.Cartridge.Service.DependentServices = $meta.Cartridge.Service.DependentServices  | Select-Object Name, DisplayName, Status
                    }
                    if ($meta.Cartridge.Service.ServicesDependedOn.Count -eq 0) {
                        $returnObj.AppInfo.JChem.Cartridge.Service.DependsOnServices = $meta.Cartridge.Service.ServicesDependedOn
                    }
                    else {
                        $returnObj.AppInfo.JChem.Cartridge.Service.DependsOnServices = $meta.Cartridge.Service.ServicesDependedOn | Select-Object Name, DisplayName, Status
                    }
                    $returnObj.AppInfo.JChem.Cartridge.Path = $meta.Cartridge.HomePath ?? $returnObj.AppInfo.JChem.Cartridge.Path
                    $returnObj.AppInfo.JChem.Cartridge.Version = $meta.Cartridge.Version ?? $returnObj.AppInfo.JChem.Cartridge.Version
                }
            }
            Get-JChemCartridgeServiceInfo
        }
        function Get-OracleAppInfo {
            if ([bool](Get-OracleDatabaseListenerProcess) -or [bool](Get-OracleDatabaseProcess)) {
                $meta = Get-OracleMetadata
                function Get-OracleDatabaseInfo {
                    $returnObj.AppInfo.Oracle.Database.Running = [bool](Get-OracleDatabaseProcess)
                    if ($returnObj.AppInfo.Oracle.Database.Running) {
                        $returnObj.AppInfo.Oracle.Database.Service.Name = $meta.Database.Service.Name ?? $returnObj.AppInfo.Oracle.Database.Service.Name
                        $returnObj.AppInfo.Oracle.Database.Service.Status = $meta.Database.Service.Status ?? $returnObj.AppInfo.Oracle.Database.Service.Status
                        $returnObj.AppInfo.Oracle.Database.Service.StartType = (($meta.Database.Service.StartType ?? $meta.Database.Service.StartMode) | Out-String -NoNewline) ?? $returnObj.AppInfo.Oracle.Database.Service.StartType
                        if ($meta.Database.Service.DependentServices.Count -eq 0) {
                            $returnObj.AppInfo.Oracle.Database.Service.DependentServices = $meta.Database.Service.DependentServices
                        }
                        else {
                            $returnObj.AppInfo.Oracle.Database.Service.DependentServices = $meta.Database.Service.DependentServices | Select-Object Name, DisplayName, Status
                        }
                        if ($meta.Database.Service.ServicesDependedOn.Count -eq 0) {
                            $returnObj.AppInfo.Oracle.Database.Service.DependsOnServices = $meta.Database.Service.ServicesDependedOn
                        }
                        else {
                            $returnObj.AppInfo.Oracle.Database.Service.DependsOnServices = $meta.Database.Service.ServicesDependedOn | Select-Object Name, DisplayName, Status
                        }
                        $returnObj.AppInfo.Oracle.Database.Path = $meta.Database.HomePath ?? $returnObj.AppInfo.Oracle.Database.Path
                        $returnObj.AppInfo.Oracle.Database.Version = $meta.Database.Version ?? $returnObj.AppInfo.Oracle.Database.Version
                    }
                }
                function Get-OracleDatabaseListenerInfo {
                    $returnObj.AppInfo.Oracle.DatabaseListener.Running = [bool](Get-OracleDatabaseListenerProcess)
                    if ($returnObj.AppInfo.Oracle.DatabaseListener.Running) {
                        $returnObj.AppInfo.Oracle.DatabaseListener.Service.Name = $meta.Listener.Service.Name ?? $returnObj.AppInfo.Oracle.DatabaseListener.Service.Name
                        $returnObj.AppInfo.Oracle.DatabaseListener.Service.Status = $meta.Listener.Service.Status ?? $returnObj.AppInfo.Oracle.DatabaseListener.Service.Status
                        $returnObj.AppInfo.Oracle.DatabaseListener.Service.StartType = (($meta.Listener.Service.StartType ?? $meta.Listener.Service.StartMode) | Out-String -NoNewline) ?? $returnObj.AppInfo.Oracle.DatabaseListener.Service.StartType
                        if ($meta.Listener.Service.DependentServices.Count -eq 0) {
                            $returnObj.AppInfo.Oracle.DatabaseListener.Service.DependentServices = $meta.Listener.Service.DependentServices
                        }
                        else {
                            $returnObj.AppInfo.Oracle.DatabaseListener.Service.DependentServices = $meta.Listener.Service.DependentServices | Select-Object Name, DisplayName, Status
                        }
                        if ($meta.Listener.Service.ServicesDependedOn.Count -eq 0) {
                            $returnObj.AppInfo.Oracle.DatabaseListener.Service.DependsOnServices = $meta.Listener.Service.ServicesDependedOn
                        }
                        else {
                            $returnObj.AppInfo.Oracle.DatabaseListener.Service.DependsOnServices = $meta.Listener.Service.ServicesDependedOn | Select-Object Name, DisplayName, Status
                        }
                        $returnObj.AppInfo.Oracle.DatabaseListener.Path = $meta.Listener.HomePath ?? $returnObj.AppInfo.Oracle.DatabaseListener.Path
                        $returnObj.AppInfo.Oracle.DatabaseListener.Version = $meta.Listener.Version ?? $returnObj.AppInfo.Oracle.DatabaseListener.Version
                    }
                }
                Get-OracleDatabaseInfo
                Get-OracleDatabaseListenerInfo
            }
        }
        function Get-AppInfo {
            Get-TomcatAppInfo
            Get-JChemAppInfo
            Get-OracleAppInfo
        }
        function Get-BrowserPlatformInfo {
            $returnObj.AppInfo.BrowserPlatform.IsBrowserSystem = Test-IsBrowserSystem
            if ($returnObj.AppInfo.BrowserPlatform.IsBrowserSystem) {
                $webAppPath = Get-TomcatWebAppPath
                $browserPropertiesFilePath = Get-BrowserPlatformPropertiesFilePath -TomcatWebAppPath $webAppPath
                if (Test-Path $browserPropertiesFilePath) {
                    $returnObj.AppInfo.BrowserPlatform.PropertiesPath = $browserPropertiesFilePath ?? $returnObj.AppInfo.BrowserPlatform.PropertiesPath
                    $returnObj.AppInfo.BrowserPlatform.Properties = (ConvertFrom-StringData (Get-Content -Raw $browserPropertiesFilePath)) ?? $returnObj.AppInfo.BrowserPlatform.Properties
                }
                $openEyeMeta = Get-OpenEyeMetadata
                $returnObj.AppInfo.BrowserPlatform.Plugins.OpenEye.UserFriendlyVersion = $openEyeMeta.UserFriendlyVersion ?? $returnObj.AppInfo.BrowserPlatform.Plugins.OpenEye.UserFriendlyVersion
                $returnObj.AppInfo.BrowserPlatform.Plugins.OpenEye.Mol2NameChecksum = $openEyeMeta.Mol2NameChecksum ?? $returnObj.AppInfo.BrowserPlatform.Plugins.OpenEye.Mol2NameChecksum
                $returnObj.AppInfo.BrowserPlatform.Plugins.OpenEye.Mol2NameBatchChecksum = $openEyeMeta.Mol2NameBatchChecksum ?? $returnObj.AppInfo.BrowserPlatform.Plugins.OpenEye.Mol2NameBatchChecksum
            }
        }
        function Get-ComputedQueries {
            $returnObj.ComputedQueries.IsBrowserPlatformSystem.Status = $returnObj.AppInfo.BrowserPlatform.IsBrowserSystem
            if ($returnObj.AppInfo.Tomcat.Running) {
                $returnObj.ComputedQueries.IsWebServer.Status = $returnObj.AppInfo.Tomcat.Running
                $returnObj.ComputedQueries.IsWebServer.AppName = "Tomcat"
            }
            if ($returnObj.AppInfo.Oracle.Database.Running) {
                $returnObj.ComputedQueries.IsDBServer.Status = $returnObj.AppInfo.Oracle.Database.Running
                $returnObj.ComputedQueries.IsDBServer.AppName = "Oracle.Database"
            }
        }
        function Get-InstanceTags {
            $instanceId = $returnObj.GenericInfo.IdentityInfo.InstanceId
            $region = $returnObj.GenericInfo.IdentityInfo.Region
            if ($instanceId -and $region) {
                $returnObj.GenericInfo.InstanceTags = Get-Ec2Tag -Filter @{Name = "resource-type"; Values = "instance" }, @{Name = "resource-id"; Values = $instanceid } -Region $region | Select-Object Key, Value
            }
        }
        function Get-StateInfo {
            $returnObj.StateInfo = Get-StateContent
        }
        Get-BasicVMInfo
        Get-AppInfo
        Get-BrowserPlatformInfo
        Get-InstanceTags
        Get-StateInfo
        Get-ComputedQueries 
        $returnObj | Write-DTXSystem
        return $returnObj
    }
    function Get-DataFromCache {
        $dtxFile = Get-DTXLocalDiscoveryFilePath
        if (!(Test-Path $dtxFile)) {
            return $null
        }
        $dtxFile = Get-Item $dtxFile
        if ($dtxFile.LastWriteTimeUtc -gt (Get-Date -AsUTC).AddHours(-1)) {
            return Get-Content -Path $dtxFile -Raw | ConvertFrom-Json -Depth 100 -AsHashtable
        }
        return $null
    }
    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Gathering system information. Please wait..."
    $returnObj = $null
    if ($NoCache) {
        $returnObj = Get-InternalDTXSystem
    }
    else {
        $cachedData = Get-DataFromCache
        if ($null -eq $cachedData) {
            $returnObj = Get-InternalDTXSystem
        }
        else {
            $returnObj = $cachedData
        }
    }
    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Information gathering complete."
    return $returnObj
}
function Get-HealthCheckInfo {
    [CmdletBinding()]
    param(
        [switch]$NoCache
    )
    function Get-InternalHealthCheck {
        $returnObj = [ordered]@{
            GenericInfo  = [ordered]@{
                Url                             = "UNKNOWN"
                PlatformVersion                 = [ordered]@{
                    BrowserVersion     = "UNKNOWN"
                    BioregisterVersion = "UNKNOWN"
                    PinpointVersion    = "UNKNOWN"
                    AllVersionsText    = "UNKNOWN"
                }
                InstanceType                    = "UNKNOWN"
                Jdk                             = "UNKNOWN"
                Antivirus                       = "UNKNOWN"
                Glowroot                        = [ordered]@{
                    IsGlowrootInJavaOptions = "UNKNOWN"
                    IsGlowrootLaunched      = "UNKNOWN"
                    TextInfo                = "UNKNOWN"
                }
                TomcatMemoryGB                  = "UNKNOWN"
                JavaTomcatOptions               = New-Object Collections.Generic.List[PSObject]
                PhysicalMemoryGB                = "UNKNOWN"
                VirtualMemoryInfo               = [ordered]@{
                    IsManagedAutomatically = $null
                    InitialSizeGB          = "UNKNOWN"
                    MaxSizeGB              = "UNKNOWN"
                    TextInfo               = "UNKNOWN"
                }
                ConnectionPoolBrowserProperties = [ordered]@{
                    maxconn = "UNKNOWN"
                    maxpool = "UNKNOWN"
                    maxwait = "UNKNOWN"
                }
                UserCountInfo                   = [ordered]@{
                    LicensesNumber = "UNKNOWN"
                    UserCountText  = "UNKNOWN"
                }
                ScheduledTasksInfo              = New-Object Collections.Generic.List[PSObject]
            }
            DatabaseInfo = [ordered]@{
                FullOracleVersion     = "UNKNOWN"
                Memory                = [ordered]@{
                    MemoryMaxTarget    = "UNKNOWN" 
                    MemoryTarget       = "UNKNOWN"
                    PgaAggregateLimit  = "UNKNOWN"
                    PgaAggregateTarget = "UNKNOWN"
                    SgaMaxSize         = "UNKNOWN"
                    SgaTarget          = "UNKNOWN"
                    TextInfo           = "UNKNOWN"
                }
                ProcessesSessions     = "UNKNOWN"
                Processes             = "UNKNOWN"
                Sessions              = "UNKNOWN"
                OpenCursors           = "UNKNOWN"
                ProjectsNumber        = "UNKNOWN"
                DatasourceCount       = [ordered]@{
                    DatasourceCountInfo       = "UNKNOWN"
                    DynamicDatasourceCount    = "UNKNOWN"
                    BrowserManagedTablesCount = "UNKNOWN"
                    BrowserManagedViewsCount  = "UNKNOWN"
                }
                MaterialisedViewCount = "UNKNOWN"
                PivotTableCount       = "UNKNOWN"
                LogsFrequencyInfo     = [ordered]@{
                    LatestLogsCheckpointNotComplete = "UNKNOWN"
                    LatestLogsHeavySwapping         = "UNKNOWN"
                    LatestLogsPgaLimit              = "UNKNOWN"
                }
            }
        }
        $dtxSystem = Get-DTXSystem
        $it = $dtxSystem.GenericInfo.InstanceTags
        $returnObj.GenericInfo.Url = (Get-InstanceUrl $it) ?? $returnObj.GenericInfo.Url
        $returnObj.GenericInfo.InstanceType = $dtxSystem.GenericInfo.IdentityInfo.instanceType
        $returnObj.GenericInfo.Jdk = $dtxSystem.AppInfo.Tomcat.Options.JVM ?? $returnObj.GenericInfo.Jdk
        $returnObj.GenericInfo.Antivirus = Get-AntivirusName ?? $returnObj.GenericInfo.Antivirus
        $returnObj.GenericInfo.Glowroot = (Get-GlowrootInfo $dtxSystem.AppInfo.Tomcat.Options.JavaOpts) ?? $returnObj.GenericInfo.Glowroot
        if ($dtxSystem.AppInfo.Tomcat.Options.JavaMaxMemoryMB -gt 100) {
            $returnObj.GenericInfo.TomcatMemoryGB = ($dtxSystem.AppInfo.Tomcat.Options.JavaMaxMemoryMB / 1024) ?? $returnObj.GenericInfo.TomcatMemoryGB
        }    
        $returnObj.GenericInfo.JavaTomcatOptions = $dtxSystem.AppInfo.Tomcat.Options.JavaOpts ?? $returnObj.GenericInfo.JavaTomcatOptions
        try {
            $returnObj.GenericInfo.PhysicalMemoryGB = ((Get-CimInstance -Class CIM_PhysicalMemory).Capacity / 1GB) ?? $returnObj.GenericInfo.PhysicalMemoryGB
        } catch {
            Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Unable to read physical data from database"
        }
        $returnObj.GenericInfo.VirtualMemoryInfo = Get-VirtualmemoryInfo ?? $returnObj.GenericInfo.VirtualMemoryInfo
        $returnObj.GenericInfo.ConnectionPoolBrowserProperties.maxconn = $dtxSystem.AppInfo.BrowserPlatform.Properties['db.maxconn'] ?? $returnObj.GenericInfo.ConnectionPoolBrowserProperties.maxconn
        $returnObj.GenericInfo.ConnectionPoolBrowserProperties.maxpool = $dtxSystem.AppInfo.BrowserPlatform.Properties['db.maxpool'] ?? $returnObj.GenericInfo.ConnectionPoolBrowserProperties.maxpool
        $returnObj.GenericInfo.ConnectionPoolBrowserProperties.maxwait = $dtxSystem.AppInfo.BrowserPlatform.Properties['db.maxwait'] ?? $returnObj.GenericInfo.ConnectionPoolBrowserProperties.maxwait
        $returnObj.GenericInfo.UserCountInfo.UserCountText = (Get-UserCountInfo $returnObj.GenericInfo.Url) ?? $returnObj.GenericInfo.UserCountInfo.UserCountText
        $returnObj.GenericInfo.UserCountInfo.LicensesNumber = (Get-LicenseNumber $webAppPath) ?? $returnObj.GenericInfo.UserCountInfo.LicensesNumber
        try {
            $returnObj.GenericInfo.ScheduledTasksInfo = (Get-ScheduledTasksInfo) ?? $returnObj.GenericInfo.ScheduledTasksInfo
        } catch {
            Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Unable to get scheduled tasks data"
        }
        $res = Get-InfoFromDatabase
        if ($res -eq $null) {
            Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Unable to get data from database"
        }
        else {
            $returnObj.DatabaseInfo.FullOracleVersion = $dtxSystem.AppInfo.Oracle.Database.Version ?? $res.OracleVersion ?? $returnObj.DatabaseInfo.FullOracleVersion
            if ($dtxSystem.AppInfo.Oracle.Database.Version -eq "UNKNOWN") {
                $returnObj.DatabaseInfo.FullOracleVersion = $res.OracleVersion ?? $returnObj.DatabaseInfo.FullOracleVersion
            }
            $returnObj.GenericInfo.PlatformVersion = $res.Version
            $returnObj.DatabaseInfo.Memory = $res.Memory
            $returnObj.DatabaseInfo.ProcessesSessions = $res.ProcessesSessions
            $returnObj.DatabaseInfo.Processes = $res.Processes
            $returnObj.DatabaseInfo.Sessions = $res.Sessions
            $returnObj.DatabaseInfo.OpenCursors = $res.OpenCursors
            $returnObj.DatabaseInfo.ProjectsNumber = $res.ProjectsNumber
            $returnObj.DatabaseInfo.DatasourceCount = $res.DatasourceCount
            $returnObj.DatabaseInfo.MaterialisedViewCount = $res.MaterialisedViewCount
            $returnObj.DatabaseInfo.PivotTableCount = $res.PivotTableCount
        }
        $fromLogs = Get-InfoFromOracleLogs
        if ($fromLogs -eq $null) {
            Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Unable to get data from database alert logs"
        }
        else {
            $returnObj.DatabaseInfo.LogsFrequencyInfo.LatestLogsCheckpointNotComplete = $fromLogs.LatestLogsCheckpointNotComplete
            $returnObj.DatabaseInfo.LogsFrequencyInfo.LatestLogsHeavySwapping = $fromLogs.LatestLogsHeavySwapping
            $returnObj.DatabaseInfo.LogsFrequencyInfo.LatestLogsPgaLimit = $fromLogs.LatestLogsPgaLimit  
        }
        $returnObj | Write-JsonCache -JsonFileName "dtx_health-check.json"
        return $returnObj
    }
    function Get-DataFromCache {
        $dtxFile = Join-Path (Get-LocalDotmaticsPath) "dtx_health-check.json"
        if (!(Test-Path $dtxFile)) {
            return $null
        }
        $dtxFile = Get-Item $dtxFile
        if ($dtxFile.LastWriteTimeUtc -gt (Get-Date -AsUTC).AddHours(-1)) {
            return Get-Content -Path $dtxFile -Raw | ConvertFrom-Json -Depth 100 -AsHashtable
        }
        return $null
    }
    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Gathering system information. Please wait..."
    $returnObj = $null
    if ($NoCache) {
        $returnObj = Get-InternalHealthCheck
    }
    else {
        $cachedData = Get-DataFromCache
        if ($null -eq $cachedData) {
            $returnObj = Get-InternalHealthCheck
        }
        else {
            $returnObj = $cachedData
        }
    }
    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Information gathering complete."
    return $returnObj
}
function Get-OracleConnectionString {
    [cmdletbinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Username,
        [Parameter(Mandatory = $true)]
        [securestring]
        $Password,
        [switch]$NoCache,
        [switch]$SkipTest
    )
    function Get-OracleConnectionStringWithoutTesting {
        [cmdletbinding()]
        param (
            [Parameter(Mandatory = $true)]
            [string]
            $Username,
            [Parameter(Mandatory = $true)]
            [securestring]
            $SecurePassword
        )
        $plainTextPassword = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecurePassword))
        $connectionDescription = Get-OracleConnectionDescription
        $connectionString = "User Id=$Username;Password=$plainTextPassword;Data Source=$connectionDescription"
        return $connectionString
    }
    $connectionString = Get-OracleConnectionStringWithoutTesting $Username $Password
    if ($SkipTest) {
        return $connectionString
    }
    if (Test-OracleConnectionString $connectionString -NoCache:$NoCache) {
        return $connectionString
    }
    else {
        Write-LogError -Message "[$($MyInvocation.MyCommand)] Connection using Oracle connect string failed"
        return "UNKNOWN"
    }
}
function Get-PatchingExitCode {
  [CmdletBinding(SupportsShouldProcess = $true)]
  param (
    [Parameter(Mandatory = $true)]
    [ValidateNotNullOrEmpty()]
    [string]$RebootExitCode
  )
  Begin {
    function Get-PatchInventoryOnWindows {
      param (
        [String] $FilePath
      )
      $hashTable = @{}
      $csvObj = Import-Csv -Delimiter ":" -Path $FilePath -Header "Key", "Value"
      foreach ($item in $csvObj) {
        $key = $item.Key.Trim()
        $value = if ($null -eq $item.Value) { $item.Value } else { $item.Value.Trim().TrimEnd(",") }
        $hashTable[$key] = $value
      }
      return $hashTable
    }
    function Get-PatchInventoryOnLinux {
      param (
        [String] $FilePath
      )
      return (get-content $FilePath  -raw | ConvertFrom-Json).Content
    }
  }
  Process {
    $rebootExitCode = [int]::Parse($RebootExitCode)
    $windowsFile = "C:\ProgramData\Amazon\PatchBaselineOperations\State\PatchInventoryFromLastOperation.json"
    $linuxFile = "/var/log/amazon/ssm/patch-configuration/patch-inventory-from-last-operation.json"
    if ($isWindows) {
      if (-not (Test-Path -Path $windowsFile)) {
        Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] No patching completed yet, not expected. Unable to find PatchInventoryFromLastOperation.json file, has something changed?" -ThrowException 
      }
      $myInventory = Get-PatchInventoryOnWindows -FilePath $windowsFile
    }
    else {
      if (-not (Test-Path -Path $linuxFile)) {
        Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] No patching completed yet, not expected. Unable to find PatchInventoryFromLastOperation.json file, has something changed?" -ThrowException 
      }
      $myInventory = Get-PatchInventoryOnLinux -FilePath $linuxFile
    }
    if ([int]$myInventory.InstalledPendingRebootCount -gt 0) {
      Write-LogInfo "[$( $MyInvocation.MyCommand )] InstalledPendingRebootCount greater than 0, current: $($myInventory.InstalledPendingRebootCount), returning reboot exit code"
      return $rebootExitCode
    }
    else {
      Write-LogInfo "[$( $MyInvocation.MyCommand )] InstalledPendingRebootCount equals 0, no reboot required"
      return 0
    }
  }
}
function Install-DTXCrowdStrikeAgent {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [string]
        $CrowdStrikeCustomerIdSSMParameterName,
        [string]
        $BucketName,
        [string]
        $BucketObjectKey,
        [ValidateNotNullOrEmpty()]
        $SupportedWindowsVersions = @("2012", "2016", "2019", "2022"),
        [switch]
        $Force
    )
    $defaults = Get-Defaults
    if (!$CrowdStrikeCustomerIdSSMParameterName) {
        $CrowdStrikeCustomerIdSSMParameterName = $defaults.AWS.SSM.ParameterStore.Parameters.CrowdStrikeCustomerId
    }
    if (!$BucketName) {
        $BucketName = $defaults.AWS.S3.Buckets.SoftwareRepository.Name
    }
    if (!$BucketObjectKey) {
        $BucketObjectKey = $defaults.AWS.S3.Buckets.SoftwareRepository.Objects.CrowdStrikeAgentInstaller.Windows
    }
    function _Test-IsWindowsVersionSupported {
        $osVersion = (Get-SystemInfo).Windows.VersionAsYear
        if ($SupportedWindowsVersions -notcontains $osVersion) {
            return $false
        }
        return $true
    }
    function _Test-IsCrowdStrikeAgentHealthy {
        $isInstalled = Test-IsAppInstalled -SearchTerm "*CrowdStrike*Win*Sensor*"
        try {
            $isServiceRunning = Test-IsServiceRunning -SearchTerm "CS*Falcon*"
        }
        catch {
            $isServiceRunning = $false
        }
        if ($isInstalled -and $isServiceRunning) {
            return $true
        }
        return $false
    }
    if ($IsWindows) {
        try {
            if ($PSCmdlet.ShouldProcess("$env:COMPUTERNAME", "Prerequisite Checks for CrowdStrike Agent")) {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Checking if the CrowdStrike Agent is installed and running..."
                if ((_Test-IsCrowdStrikeAgentHealthy) -and (-not $Force)) {
                    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] The CrowdStrike Agent is already installed and running."
                    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] No action required."
                    return
                }
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] The CrowdStrike Agent is not installed or running. Starting installation..."
                if (-not (_Test-IsWindowsVersionSupported)) {
                    Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] The CrowdStrike Agent is not supported on this version of Windows. Supported Windows versions are $($SupportedWindowsVersions -join ', ')." -ThrowException
                }
            }
            if ($PSCmdlet.ShouldProcess( "$env:COMPUTERNAME", "Download CrowdStrike Agent" )) {
                $installationFile = Get-FileFromS3 -BucketName $BucketName -BucketKey $BucketObjectKey -PreserveFileName
            }
            if ($PSCmdlet.ShouldProcess("$env:COMPUTERNAME", "Install CrowdStrike Agent")) {
                $customerId = (Get-SSMParameterValue -Name $CrowdStrikeCustomerIdSSMParameterName -Region $defaults.AWS.SSM.ParameterStore.DefaultRegion -WithDecryption:$true -ErrorAction Stop).Parameters[0].Value
                Install-CrowdStrikeAgent -CustomerId $customerId -InstallationFile $installationFile
                Remove-Item -Path $installationFile -Force
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] The CrowdStrike Agent has been installed successfully."
            }
        }
        catch {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] Error message: $($_.Exception.Message)" -ThrowException -Exception $_
        }
    }
    else {
        Write-LogInfo "[$( $MyInvocation.MyCommand )] Installing the CrowdStrike Agent using this module is only supported on Windows for now."
    }
}
function Install-DTXOpenEye {
    param(
        [Parameter(Mandatory)]
        [ValidateSet(
            "2024"
        )]
        [string]$Version,
        [switch]$Backup,
        [switch]$Force
    )
    $ErrorActionPreference = "Stop"
    if ($IsWindows) {
        try {
            Show-Banner -AsLog -Message "Start -> Dotmatics - Open Eye Installer Automation"
            Set-StateItem -Key "openeye-install-check-date" -Value (Get-Date -Format "yyyy-MM-dd")
            Write-LogInfo -Message "[$($MyInvocation.MyCommand)] Checking if this is a Browser System..."
            $isBrowserPlatformSystem = (Get-DTXSystem).ComputedQueries.IsBrowserPlatformSystem.Status
            if ((($isBrowserPlatformSystem -is [string]) -and ($isBrowserPlatformSystem -eq "UNKNOWN")) -or !$isBrowserPlatformSystem) {
                Write-LogInfo -Message "[$($MyInvocation.MyCommand)] This instance is not a browser instance. Skipping Open Eye installation."
                return
            }
            Write-LogInfo -Message "[$($MyInvocation.MyCommand)] This is a Browser System."
            if (!$Force) {
                if (Test-OpenEyeIsUpToDate -Version $Version) {
                    return
                }
            }
            if ($Backup) {
                Backup-OpenEye | Out-Null
            }
            Install-OpenEye -Version $Version | Out-Null
            Set-StateItem -Key "openeye-install-version" -Value $Version | Out-Null
            Set-StateItem -Key "openeye-install-date" -Value (Get-Date -Format "yyyy-MM-dd") | Out-Null
            Remove-OpenEyeBackup -BackupsToKeep 1 | Out-Null
        }
        finally {
            Show-Banner -AsLog -Message "Stop -> Dotmatics - Open Eye Installer Automation"
        }
    }
    else {
        Write-LogWarning -Message "[$($MyInvocation.MyCommand)] This cmdlet is only supported on Windows operating systems. "
    }
}
function Install-DTXSumoLogicAgent{
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [ValidateNotNullOrEmpty()]
        $SupportedWindowsVersions = @("2012", "2016", "2019", "2022"),
        [switch]
        $Force
    )
    function _Test-IsWindowsVersionSupported {
        $osVersion = (Get-SystemInfo).Windows.VersionAsYear
        if ($SupportedWindowsVersions -notcontains $osVersion) {
            return $false
        }
        return $true
    }    
    try {
        Show-Banner -AsLog -Message "Start -> Dotmatics - Summologic Agent Install"
        if ($IsLinux -or $IsMacOS) {
            Write-LogWarning -Message "[$($MyInvocation.MyCommand)] This cmdlet is only supported on Windows operating systems. "
            return
        }
        if (-not (_Test-IsWindowsVersionSupported)) {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] The SumoLogic Agent is not supported on this version of Windows. Supported Windows versions are $($SupportedWindowsVersions -join ', ')." -ThrowException
        }
        $sumoLogicServiceName="sumo-collector"
        if (Get-Service -ErrorAction SilentlyContinue | Where-Object { $_.Name -eq $sumoLogicServiceName}) {
            $sumoLogicService = Get-Service -Name $sumoLogicServiceName -ErrorAction Stop
            if ($sumoLogicService.Status -ne 'Running') {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Service '$sumoLogicServiceName' is not running. Attempting to start it..."
                if ($PSCmdlet.ShouldProcess("$env:COMPUTERNAME", "Starting Sumologic Agent")) {
                    Start-Service -Name $sumoLogicServiceName
                }
                $sumoLogicService = Get-Service -Name $sumoLogicServiceName -ErrorAction Stop
                $status = $sumoLogicService.Status
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Service '$sumoLogicServiceName' status: $status"
            } else {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Service '$sumoLogicServiceName' exist and running."
            }
        }else{
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Service '$sumoLogicServiceName' does not exists. Attempting to install..."
            $defaults = Get-Defaults
            $SumoLogicAccessIdSSMParameterName = $defaults.AWS.SSM.ParameterStore.Parameters.SumoLogicAccessId
            $SumoLogicAccessKeySSMParameterName = $defaults.AWS.SSM.ParameterStore.Parameters.SumoLogicAccessKey
            $BucketName = $defaults.AWS.S3.Buckets.SoftwareRepository.Name
            $BucketObjectKey = $defaults.AWS.S3.Buckets.SoftwareRepository.Objects.SumoLogicAgentInstaller.Windows
            $sumoLogicAccessId=(Get-SSMParameterValue -Name $SumoLogicAccessIdSSMParameterName -Region $defaults.AWS.SSM.ParameterStore.DefaultRegion -WithDecryption:$true -ErrorAction Stop).Parameters[0].Value
            $sumoLogicAccessKey=(Get-SSMParameterValue -Name $SumoLogicAccessKeySSMParameterName -Region $defaults.AWS.SSM.ParameterStore.DefaultRegion -WithDecryption:$true -ErrorAction Stop).Parameters[0].Value
            if ($PSCmdlet.ShouldProcess("$env:COMPUTERNAME", "Install Sumologic Agent")) {
                Install-SumoLogic -AccessId $sumoLogicAccessId -AccessKey $sumoLogicAccessKey -BucketName $BucketName -BucketObjectKey $BucketObjectKey
            }
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Service '$sumoLogicServiceName' installed sucessfully and running."
        }
    }
    finally {
        Show-Banner -AsLog -Message "Stop -> Dotmatics - SumoLogic Install"
    }
}
function Install-DTXVortexWebOE {
    param(
        [Parameter(Mandatory)]
        [ValidateSet(
            "6.1.2.1372-s"
        )]
        [string]$Version
    )
    $ErrorActionPreference = "Stop"
    try {
        Show-Banner -AsLog -Message "Start -> Dotmatics - VortexWeb Install"
        if ($IsLinux -or $IsMacOS) {
            Write-LogWarning -Message "[$($MyInvocation.MyCommand)] This cmdlet is only supported on Windows operating systems. "
            return
        }
        if (!(Test-IsBrowserSystem)) {
            Write-LogWarning "[$( $MyInvocation.MyCommand )] Is not a Browser system, nothing to do here"
            return
        }
        $VortexVersion = -1
        $SysMonitorData = Get-BrowserSysMonitor
        if ($SysMonitorData.ContainsKey("vortexweb_version")) {
            $VortexVersion = [int]$SysMonitorData.vortexweb_version
        }
        Write-LogInfo "[$($MyInvocation.MyCommand)] Detected Vortex Version: $($VortexVersion)"
        if ($VortexVersion -ge 1124 -and $VortexVersion -le 1140) {
            Install-VortexWeb -Version $Version
        }
        else {
            Write-LogInfo "[$($MyInvocation.MyCommand)] Vortex does not need to be replaced"
        }
    }
    finally {
        Show-Banner -AsLog -Message "Stop -> Dotmatics - VortexWeb Install"
    }
}
function Invoke-DTXTomcatChanges {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param()
    function Get-TomcatChangeList {
        param (
            [hashtable]$DTXSystemInfo,
            [hashtable]$DTXDefaults
        )
        $changeCandidates = @( 
            {
                Write-LogInfo -Message "Initializing DTXTomcatChangeCertificate"
                [DTXTomcatChangeCertificate]::new($DTXSystemInfo, $DTXDefaults)
            }, 
            {
                Write-LogInfo -Message "Initializing DTXTomcatChangeConnectorSettings"
                [DTXTomcatChangeConnectorSettings]::new($DTXSystemInfo, $DTXDefaults, 2)
            }
        )
        $changeList = @()
        foreach ($candidate in $changeCandidates) {
            try {
                $changeList += & $candidate
            }
            catch {
                Write-LogWarning -Message "Failed to initialize. Error: $_"
            }
        }
        return $changeList
    }
    function Test-IsTomcatWebServer {
        param (
            [hashtable]$DTXSystemInfo
        )
        return ($DTXSystemInfo.ComputedQueries.IsWebServer.Status -eq $true -and $DTXSystemInfo.ComputedQueries.IsWebServer.AppName -eq "Tomcat")
    }
    function Test-IsTomcatChangeRequired {
        param (
            [array]$ChangeList
        )
        $changeRequired = $false
        foreach ($item in $ChangeList) {
            if ($item.ChangeRequired()) {
                $changeRequired = $true
            }
        }
        return $changeRequired
    }
    function Start-TomcatService {
        [CmdletBinding(SupportsShouldProcess = $true)]
        param (
            [string]$ServiceName
        )
        if ($PSCmdlet.ShouldProcess($ServiceName, "Start")) {
            Write-LogInfo -Message "[$($MyInvocation.InvocationName)] Starting Tomcat Service: $($ServiceName)"
            Start-Service -Name $ServiceName
        }
    }
    function Stop-TomcatService {
        [CmdletBinding(SupportsShouldProcess = $true)]
        param (
            [string]$ServiceName
        )
        if ($PSCmdlet.ShouldProcess($ServiceName, "Stop")) {
            Write-LogInfo -Message "[$($MyInvocation.InvocationName)] Stopping Tomcat Service: $($ServiceName)"
            Stop-ServiceWithRetry -Name $ServiceName -Retries 40 -WaitSeconds 15
        }
    }
    function Invoke-TomcatChanges {
        [CmdletBinding(SupportsShouldProcess = $true)]
        param (
            [array]$ChangeList
        )
        foreach ($item in $ChangeList) {
            if ($PSCmdlet.ShouldProcess($item.GetType().Name, "Performed Tomcat change")) {
                $item.PerformChange()
            }
        }
    }
    function Invoke-TomcatTests {
        param (
            [array]$ChangeList
        )
        foreach ($item in $ChangeList) {
            Write-LogInfo -Message "[$($MyInvocation.InvocationName)] Performing testing for $($item.GetType().Name)"
            $item.TestAfterChange()
        }
    }
    function Wait-ForTomcatToServeRequests {
        param (
            [int]$Port
        )
        Write-LogInfo -Message "[$($MyInvocation.InvocationName)] Giving Tomcat a chance to start serving requests before running tests..."
        Wait-ForWebServerToServeRequests -Hostname "localhost" -Port $Port -Scheme "https" -WaitSeconds 900 -SkipCertificateCheck
    }
    Show-Banner -AsLog -Message "Start -> Dotmatics - Tomcat Changes"
    $dtxDefaults = Get-Defaults
    $dtxSystemInfo = Get-DTXSystem
    $tomcatServiceName = $dtxSystemInfo.AppInfo.Tomcat.Service.Name
    $tomcatHttpPort = $dtxSystemInfo.AppInfo.Tomcat.ServerXML.Port
    if (-not (Test-IsTomcatWebServer -DTXSystemInfo $dtxSystemInfo)) {
        Write-LogInfo  -Message "[$( $MyInvocation.MyCommand )] Not a Tomcat Webserver. No changes required."
        return
    }
    $tomcatChangeList = Get-TomcatChangeList -DTXSystemInfo $dtxSystemInfo -DTXDefaults $dtxDefaults
    Write-LogInfo -Message "[$($MyInvocation.MyCommand)] Checking if change is required..."
    if (-not (Test-IsTomcatChangeRequired -ChangeList $tomcatChangeList)) {
        Write-LogInfo -Message "[$($MyInvocation.MyCommand)] No changes required."
        return
    }
    Write-LogInfo -Message "[$($MyInvocation.MyCommand)] Change is required."
    try {
        Stop-TomcatService -ServiceName $tomcatServiceName
        Invoke-TomcatChanges -ChangeList $tomcatChangeList
    }
    finally {
        Start-TomcatService -ServiceName $tomcatServiceName
    }
    Wait-ForTomcatToServeRequests -Port $tomcatHttpPort
    Invoke-TomcatTests -ChangeList $tomcatChangeList
    Show-Banner -AsLog -Message "Stop -> Dotmatics - Tomcat Changes"
}
function Invoke-EC2Authentication {
    param (
        [Parameter(Mandatory)]
        [string]
        $IAMRoleArn,
        [bool]$DryRun = $false
    )
    if ($DryRun) {
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Running Invoke-EC2Authentication in dry run mode. No action will be taken."
        return
    }
    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Assuming IAM role: $IAMRoleArn"
    try {
        Set-DefaultAWSRegion -Region (Get-EC2InstanceMetadata -Category Region -ErrorAction Stop).SystemName -ErrorAction Stop -Scope Global
    }
    catch {
        Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] This script must be run on an EC2 instance that has access to the EC2 instance metadata service." -ThrowException -Exception $_
    }
    try {
        $c = (Use-STSRole -RoleSessionName (New-Guid).Guid -RoleArn $IAMRoleArn -DurationInSeconds 3600 -ErrorAction Stop).Credentials 
        Set-AWSCredential -AccessKey $c.AccessKeyId -SecretKey $c.SecretAccessKey -SessionToken $c.SessionToken -ErrorAction Stop -Scope Global
    }
    catch {
        Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] Failed to assume IAM role: $IAMRoleArn." -ThrowException -Exception $_
    }
    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Successfully assumed IAM role: $IAMRoleArn"
}
function Invoke-HookPreReboot {
    param(
        [Parameter(Mandatory)]
        [bool]
        $DryRun,
        [Parameter(Mandatory)]
        [bool]
        $RebootRequested,
        [Parameter(Mandatory)]
        [bool]
        $InMaintWindow,
        [Parameter(Mandatory)]
        [int]
        $RebootExitCode,
        [Parameter(Mandatory)]
        [int]
        $ContinueOnErrorExitCode,
        [Parameter(Mandatory)]
        [string]
        $EC2Role
    )
    if ($IsWindows) {
        function Stop-Tomcat {
            param(
                [psobject] $Meta,
                [bool] $DryRun
            )
            if ($DryRun) {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Dry Run: Stopping the Tomcat service"
                return
            }
            $app = $Meta.AppInfo.Tomcat
            if ($app.Running -and ($app.Running -is [bool])) {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Stopping the Tomcat service..."
                try {
                    Stop-ServiceWithRetry -Name $app.Service.Name -Retries 18 -WaitSeconds 5
                }
                catch {
                    Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] The Tomcat service did not stop in time."
                    return
                }
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Stopped."
            }
            else {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] No running Tomcat service found."
            }
        }
        function Stop-OracleDatabaseListener {
            param(
                [psobject] $Meta,
                [bool] $DryRun
            )
            if ($DryRun) {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Dry Run: Stopping the Oracle Database Listener service"
                return
            }
            $app = $Meta.AppInfo.Oracle.DatabaseListener
            if ($app.Running -and ($app.Running -is [bool])) {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Stopping the Oracle Database Listener service..."
                try {
                    Stop-ServiceWithRetry -Name $app.Service.Name -Retries 12 -WaitSeconds 5 
                }
                catch {
                    Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] The Oracle Database Listener service did not stop in time."
                    return
                }
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Stopped."
            }
            else {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] No running Oracle Database Listener service found."
            }
        }
        function Stop-OracleDatabase {
            param(
                [psobject] $Meta,
                [bool] $DryRun
            )
            if ($DryRun) {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Dry Run: Stopping the Oracle Database service"
                return
            }
            $app = $Meta.AppInfo.Oracle.Database
            if ($app.Running -and ($app.Running -is [bool])) {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Stopping the Oracle Database service..."
                try {
                    Stop-ServiceWithRetry -Name $app.Service.Name -Retries 36 -WaitSeconds 5
                }
                catch {
                    Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] The Oracle Database service did not stop in time."
                    return
                }
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Stopped."
            }
            else {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] No running Oracle Database service found."
            }
        }
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Starting Invoke-HookPreReboot..."
        try {
            Write-LogSeparator
            Invoke-EC2Authentication -IAMRoleArn $EC2Role
            Write-LogSeparator
            Write-LogSeparator
            $meta = Get-DTXSystem
            Write-LogSeparator
            Write-LogSeparator
            Stop-Tomcat -Meta $meta -DryRun:$DryRun
            Write-LogSeparator
            Write-LogSeparator
            Stop-OracleDatabaseListener -Meta $meta -DryRun:$DryRun
            Write-LogSeparator
            Write-LogSeparator
            Stop-OracleDatabase -Meta $meta -DryRun:$DryRun
            Write-LogSeparator
        }
        catch {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] An error occurred."
            $_ | Get-Error | Out-Host
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] Exiting with code 1."
            exit 1
        }
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Completed Invoke-HookPreReboot. Exiting with code 0."
    }
    else {
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] The pre reboot hook is supported only on Windows for now. Exiting with code 0."
    }
    exit 0
}
function Invoke-NonOutageAutomation {
    param(
        [Parameter(Mandatory)]
        [bool]
        $DryRun,
        [Parameter(Mandatory)]
        [bool]
        $RebootRequested,
        [Parameter(Mandatory)]
        [bool]
        $InMaintWindow,
        [Parameter(Mandatory)]
        [int]
        $RebootExitCode,
        [Parameter(Mandatory)]
        [int]
        $ContinueOnErrorExitCode,
        [Parameter(Mandatory)]
        [string]
        $EC2Role
    )
    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Starting Invoke-NonOutageAutomation..."
    try {
        Write-LogSeparator
        Invoke-EC2Authentication -IAMRoleArn $EC2Role 
        Write-LogSeparator
        Write-LogSeparator
        New-DTXDiscoveryJsonFile -WhatIf:$DryRun
        Write-LogSeparator
        Write-LogSeparator
        Install-DTXOpenEye -Version "2024" -Backup
        Write-LogSeparator
        Write-LogSeparator
        Install-DTXCrowdStrikeAgent -WhatIf:$DryRun
        Write-LogSeparator
        Write-LogSeparator
        Install-DTXSumoLogicAgent -WhatIf:$DryRun
        Write-LogSeparator
        Write-LogSeparator
        Sync-StarDotmaticsNetCertificate -WhatIf:$DryRun
        Write-LogSeparator
        Write-LogSeparator
        Uninstall-DTXAutomoxAgent -WhatIf:$DryRun
        Write-LogSeparator
    }
    catch {
        Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] An error occurred."
        $_ | Get-Error | Out-Host
        Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] Exiting with code $ContinueOnErrorExitCode."
        exit $ContinueOnErrorExitCode
    }
    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Completed Invoke-NonOutageAutomation. Exiting with code 0"
    exit 0
}
function Invoke-OutageAutomation {
    param(
        [Parameter(Mandatory)]
        [bool]
        $DryRun,
        [Parameter(Mandatory)]
        [bool]
        $RebootRequested,
        [Parameter(Mandatory)]
        [bool]
        $InMaintWindow,
        [Parameter(Mandatory)]
        [int]
        $RebootExitCode,
        [Parameter(Mandatory)]
        [int]
        $ContinueOnErrorExitCode,
        [Parameter(Mandatory)]
        [string]
        $EC2Role,
        [switch]
        $ExitWithRebootExitCode,
        [switch]
        $ExitWithContinueOnErrorExitCode,
        [switch]
        $ExitWithErrorExitCode
    )
    function Invoke-ExitCodeTesting() {
        if ($ExitWithRebootExitCode) {
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Exiting with RebootExitCode: $RebootExitCode"
            exit $RebootExitCode
        }
        if ($ExitWithContinueOnErrorExitCode) {
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Exiting with ContinueOnErrorExitCode: $ContinueOnErrorExitCode"
            exit $ContinueOnErrorExitCode
        }
        if ($ExitWithErrorExitCode) {
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Exiting with error: 1"
            exit 1
        }
    }
    function Remove-InvalidScheduledTasks() {
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Removing Invalid Scheduled Tasks"
        $invalidScheduledTasks = (Get-Defaults).PrivateData.ScheduledTasksToRemove
        $invalidScheduledTasks | Remove-ScheduledTasks -WhatIf:$DryRun
    }
    function Invoke-ContinueOnErrorAutomation() {
        try {
            Write-LogSeparator
            Invoke-EC2Authentication -IAMRoleArn $EC2Role 
            Write-LogSeparator
            Write-LogSeparator
            Remove-InvalidScheduledTasks
            Write-LogSeparator
            Write-LogSeparator
            Install-DTXVortexWebOE -Version "6.1.2.1372-s"
            Write-LogSeparator
            Write-LogSeparator
            Invoke-DTXTomcatChanges
            Write-LogSeparator
        }
        catch {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] An error occurred."
            $_ | Get-Error | Out-Host
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] Exiting in ContinueOnerror with code $ContinueOnErrorExitCode."
            exit $ContinueOnErrorExitCode
        }
    }
    function Invoke-ExitOnErrorAutomation() {
        try {
        }
        catch {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] An error occurred."
            $_ | Get-Error | Out-Host
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] Exiting in Stop on Failure with code 1."
            exit 1
        }
    }
    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Starting Invoke-OutageAutomation..."
    Invoke-ExitCodeTesting
    Invoke-ContinueOnErrorAutomation
    Invoke-ExitOnErrorAutomation
    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Completed Invoke-OutageAutomation. Exiting with code 0"
    exit 0
}
function New-DTXCloudEnvironment {
    [CmdletBinding(ConfirmImpact = "High", SupportsShouldProcess)]
    Param(
        [Parameter(Mandatory)]
        [ValidatePattern('^[a-zA-Z0-9-]+$')]
        [string]
        $CustomerName,
        [Parameter(Mandatory)]
        [ValidatePattern('^https:\/\/.+\.dotmatics\.net$')]
        [string]
        $URL,
        [Parameter(Mandatory)]
        [ValidatePattern('^\d{12}$')]
        [string]
        $AWSAccountNumber,
        [Parameter(Mandatory)]
        [ValidateSet(
            "ap-northeast-1",
            "ap-southeast-1",
            "ap-southeast-2",
            "eu-central-1",
            "eu-west-1",
            "eu-west-2",
            "us-east-1",
            "us-west-1",
            "sa-east-1",
            IgnoreCase = $true
        )]
        [string]
        $Region,
        [Parameter(Mandatory)]
        [string]
        $VPCId,
        [Parameter(Mandatory)]
        [string]
        $SubnetId,
        [Parameter(Mandatory)]
        [string]
        $AMIId,
        [Parameter(Mandatory)]
        [string]
        $InstanceType,
        [Parameter(Mandatory)]
        [ValidateSet(
            "customer_production",
            "customer_non_production",
            "internal",
            IgnoreCase = $true
        )]
        [string]
        $EnvironmentType,
        [Parameter(Mandatory)]
        [ValidatePattern('\b[a-z0-9]\w{4}0\w{12}|[a-z0-9]\w{4}0\w{9}\b')]
        [string]
        $SalesforceAccountId,
        [Parameter(Mandatory)]
        [ValidateSet(
            "persistent",
            "temporary",
            IgnoreCase = $true
        )]
        [string]
        $InstancePersistence,
        [datetime]
        $InstancePersistenceEndTime,
        [Parameter(Mandatory)]
        [ValidateSet(
            "cloud",
            "customer_support",
            "customer_system",
            "devops_luma",
            "devops_platform",
            "engineering_luma",
            "engineering_platform",
            "global_service",
            "presales",
            "qa_luma",
            "qa_platform",
            "sts",
            "techops",
            "upgrades",
            IgnoreCase = $true
        )]
        [string]
        $Department,
        [Parameter(Mandatory)]
        [ValidateSet(
            "platform",
            "luma",
            "external_product",
            IgnoreCase = $true
        )]
        $ProductName,
        [Parameter(Mandatory)]
        [ValidateSet(
            "TRUE",
            "FALSE",
            IgnoreCase = $false
        )]
        [string]
        $CustomerAccess,
        [Parameter(Mandatory)]
        [ValidateSet(
            "pre_prod",
            "prod",
            IgnoreCase = $false
        )]
        [string]
        $PatchGroup,
        [Parameter(Mandatory)]
        [ValidateSet(
            "pre_prod_sun",
            "pre_prod_sat",
            "prod_sun",
            "prod_sat",
            IgnoreCase = $false
        )]
        [string]
        $AutoExecution,
        [string]
        $PRTGUsername,
        [string]
        $PRTGPassword,
        [string]
        $ADDomainUser = "SSM",
        [string]
        $ADDomainPassword = "SSM",
        [string]
        $PRTGHostname,
        [string]
        $PRTGTemplateDeviceId = "5805",
        [string]
        $Route53HostedZoneId,
        [string]
        $TemporaryIamProfileName,
        [String]
        $PersistentIamProfileName,
        [Parameter(Mandatory = $true)]
        [string]
        $TicketNumber, 
        [switch]
        $SkipTranscript,
        [switch]
        $SkipDomainJoin,
        [switch]
        $SkipMonitoring,
        [switch]
        $SkipPostDeployment,
        [switch]
        $SkipDuo,
        [switch]
        $ResetLocalCache,
        [string]
        $SoftwareRepositoryBucketName
    )
    try {
        if ($null -eq $env:DTX_LOCAL_DOTMATICS_PATH) {
            $env:DTX_LOCAL_DOTMATICS_PATH = Join-Path $HOME ".local" "dotmatics"
            $env:DTX_LOCAL_DOTMATICS_PATH_WAS_SET = $true
        }
        If (-not $SkipTranscript) {
            $transcriptFile = "$( Get-Random ).txt"
            $transcriptFilePath = Join-Path -Path $([System.IO.Path]::GetTempPath() ) -ChildPath $transcriptFile
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Transcript started, output file is $transcriptFilePath"
            [void]$( Start-Transcript -Path $transcriptFilePath )
        }
        if ($InstancePersistence -eq "temporary" -and (-not $InstancePersistenceEndTime)) {
            Write-LogCritical -ThrowException -Message "The 'InstancePersistenceEndTime' parameter is required when the 'InstancePersistence' parameter is set to 'temporary'."
        }
        $inputParamsToHash = @(
            $CustomerName
            $Region
            $EnvironmentType
            $URL
            $SalesforceAccountId
            $InstancePersistence
            $Department
            $ProductName
        )
        $inputParamsHash = Get-StringHash -String ($inputParamsToHash -join "")
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Input parameters hash: $inputParamsHash"
        $kvStorePath = Join-Path -Path ([System.IO.Path]::GetTempPath()) "$inputParamsHash.json"
        $kvStore = [DTXKeyValueStore]::new($kvStorePath)
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Key value store path: $kvStorePath"
        if ($ResetLocalCache) { $kvStore.ResetStore() }
        if ($kvStore.GetValue("IsDeploymentCompleted")) {
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] The deployment of $instanceName has already been completed."
            if (Read-BasicUserResponse -Prompt "Do you want to restart the deployment? (y/n)") {
                $kvStore.ResetStore()
            }
            else {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] The deployment of $instanceName has been cancelled."
                return
            }
        }
        $defaults = Get-Defaults
        if (-not $SkipMonitoring) {
            $PRTGHostname = $defaults.PRTG.Hostname
        }
        if (-not $Route53HostedZoneId) {
            $Route53HostedZoneId = $defaults.AWS.Route53.HostedZones.DotmaticsNet.Id
        }
        if (-not $TemporaryIamProfileName) {
            $TemporaryIamProfileName = $defaults.AWS.IAM.InstanceProfiles.Temporary.Name
        }
        if (-not $PersistentIamProfileName) {
            $PersistentIamProfileName = $defaults.AWS.IAM.InstanceProfiles.Persistent.Name
        }
        if (-not $SoftwareRepositoryBucketName) {
            $SoftwareRepositoryBucketName = $defaults.AWS.S3.Buckets.SoftwareRepository.Name
        }
        $randomString = ($kvStore.GetValue("RandomString")) ?? (Get-RandomString -Length 4)
        $kvStore.SetValue("RandomString", $randomString)
        $envType = Format-EnvironmentType -Environment $EnvironmentType
        $instanceName = Get-InstanceName -CustomerName $CustomerName -Environment $envType.MediumName -RandomString $randomString -Region $Region
        $computerName = Get-ComputerName -CustomerName $CustomerName -Environment $envType.ShortName -RandomString $randomString
        $customTags = @{
            'customer-access'      = $CustomerAccess.ToUpper()
            'department'           = $Department.ToLower()
            'environment-type'     = $envType.TagValue.ToLower()
            'hostname'             = $computerName
            'persistence'          = $InstancePersistence.ToLower()
            'product'              = $ProductName.ToLower()
            'sfdc-account-id'      = $SalesforceAccountId
            'system-creation-date' = (Get-Date -AsUTC -Format "yyyy-MM-dd").ToString()
            'ticket-number'        = $TicketNumber
            'PatchGroup'           = $PatchGroup.ToLower()
            'auto_execution'       = $AutoExecution.ToLower()
            'Url'                  = $URL
        }
        if ($InstancePersistenceEndTime) {
            $customTags.Add("persistence-end-time", $InstancePersistenceEndTime.ToString("yyyy-MM-dd"))
        }
        $domainController = Get-PreferredDomainController -Region $Region
        Confirm-AWSCredentials -Region $Region -AWSAccountNumber $AWSAccountNumber
        Confirm-InstanceName -InstanceName $instanceName -Region $Region
        Confirm-ComputerName -ComputerName $computerName -DomainControllerInstanceId $domainController.InstanceId -DomainControllerRegion $domainController.Region
        Confirm-AWSVPC -Region $Region -VPCId $VPCId
        Confirm-AWSSubnet -Region $Region -VPCId $VPCId -SubnetId $SubnetId
        Confirm-AWSAMI -Region $Region -Id $AMIId
        if (-not $SkipMonitoring) { Confirm-PRTGConnection -Username $PRTGUsername -Password $PRTGPassword -Hostname $PRTGHostname }
        Confirm-AWSElasticIPQuota -Region $Region
        Confirm-AWSEC2InstanceQuota -Region $Region -InstanceType $InstanceType
        $awsManagementSecurityGroupIds = Get-AWSManagementSecurityGroups -Region $Region -VPCId $VPCId
        $newAWSSecurityGroupParams = @{
            Name        = $instanceName
            Description = "Security rules for customer $CustomerName and instance $instanceName."
            VPCId       = $VPCId
            Region      = $Region
            Tags        = $customTags.Clone()
        }
        $awsManagementSecurityGroupIds += New-AWSSecurityGroup @newAWSSecurityGroupParams
        $newAWSEC2InstanceParams = @{
            InstanceName           = $instanceName
            InstanceType           = $InstanceType
            InstanceIamProfileName = $TemporaryIamProfileName
            SubnetId               = $SubnetId
            SecurityGroupIds       = $awsManagementSecurityGroupIds
            AMIId                  = $AMIId
            Region                 = $Region
            Tags                   = $customTags.Clone()
        }
        $awsEC2Instance = New-AWSEC2Instance @newAWSEC2InstanceParams
        $newAWSElasticIpParams = @{
            InstanceName        = $instanceName
            InstanceId          = $awsEC2Instance.InstanceId
            Region              = $Region
            AttachToEC2Instance = $true
            Tags                = $customTags.Clone()
        }
        $AWSElasticIp = New-AWSElasticIp @newAWSElasticIpParams
        if ($kvStore.GetValue("IsSSMAssociationCompleted")) {
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] The SSM association has already been completed."
        }
        else {
            Wait-ForSSMAssociation -InstanceId $awsEC2Instance.InstanceId -Region $Region | Out-Null
            $kvStore.SetValue("IsSSMAssociationCompleted", $true)
        }
        if ($kvStore.GetValue("IsAutomationDependenciesInstalled")) {
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] The automation dependencies have already been installed."
        }
        else {
            Install-PowerShellCoreViaSSM -InstanceId $awsEC2Instance.InstanceId -Region $Region -PatchGroup $PatchGroup | Out-Null
            Install-AutomationDependencies -InstanceId $awsEC2Instance.InstanceId -Region $Region | Out-Null
            $kvStore.SetValue("IsAutomationDependenciesInstalled", $true)
        }
        if ($kvStore.GetValue("IsWindowsHostnameSet")) {
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] The Windows hostname has already been set."
        }
        else {
            $setWindowsHostnameParams = @{
                Name       = $computerName
                InstanceId = $awsEC2Instance.InstanceId
                Region     = $Region
            }
            [void]$( Set-WindowsHostname @setWindowsHostnameParams )
            $kvStore.SetValue("IsWindowsHostnameSet", $true)
        }
        if (-not $SkipDomainJoin) {
            if ($kvStore.GetValue("IsADDomainJoinCompleted")) {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] The instance has already been joined to the active directory domain."
            }
            else {
                $invokeADDomainJoinParams = @{
                    InstanceId = $awsEC2Instance.InstanceId
                    Region     = $Region
                }
                [void]$( Invoke-ADDomainJoin @invokeADDomainJoinParams )
                $kvStore.SetValue("IsADDomainJoinCompleted", $true)
            }
        }
        $newAWSRoute53ARecordParams = @{
            Name         = $instanceName
            IPAddress    = $AWSElasticIp.PublicIp
            HostedZoneId = $Route53HostedZoneId
            Region       = $Region
        }
        $awsRoute53ARecord = New-AWSRoute53ARecord @newAWSRoute53ARecordParams
        $newAWSRoute53CnameRecordParams = @{
            Name         = (($URL -split "//")[1] -split "\.")[0].ToLower()
            Target       = $awsRoute53ARecord.Name.Trim(".")
            HostedZoneId = $Route53HostedZoneId
            Region       = $Region
        }
        [void]$(New-AWSRoute53CnameRecord @newAWSRoute53CnameRecordParams)
        if ($kvStore.GetValue("IsTimeZoneSet")) {
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] The time zone has already been set."
        }
        else {
            $setTimeZoneParams = @{
                InstanceId = $awsEC2Instance.InstanceId
                Region     = $Region
            }
            [void]$(Set-TimeZone @setTimeZoneParams)
            $kvStore.SetValue("IsTimeZoneSet", $true)
        }
        $newADSecurityGroupParams = @{
            Name                       = $instanceName
            Description                = "Manage local admin access for customer: $CustomerName"
            Region                     = $domainController.Region
            DomainControllerInstanceId = $domainController.InstanceId
            Members                    = @($defaults.ActiveDirectory.Groups.CA.Name)
        }
        New-ADSecurityGroup @newADSecurityGroupParams
        if (-not $SkipPostDeployment) {
            if ($kvStore.GetValue("IsPostDeploymentTasksCompleted")) {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] The post deployment tasks have already been run."
            }
            else {
                if (-not $SkipDomainJoin) {
                    $domainName = ($defaults.ActiveDirectory.DomainName).ToLower()
                    [void]$( Add-InstanceLocalGroupMember -LocalGroup "Administrators" -Member "$domainName\$instanceName" -InstanceId $awsEC2Instance.InstanceId -Region $Region )
                    [void]$( Add-InstanceLocalGroupMember -LocalGroup "ORA_DBA" -Member "$domainName\$instanceName" -InstanceId $awsEC2Instance.InstanceId -Region $Region )
                }
                $postDeploymentTasksParams = @{
                    InstanceId               = $awsEC2Instance.InstanceId
                    Region                   = $Region
                    AccessUrl                = $URL
                    ActiveDirectoryGroupName = $instanceName
                }
                [void]$( Invoke-PostDeploymentTasks @postDeploymentTasksParams )
                $kvStore.SetValue("IsPostDeploymentTasksCompleted", $true)
            }
        }
        if (-not $SkipMonitoring) {
            if ($kvStore.GetValue("IsMonitoringCompleted")) {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] The monitoring has already been completed."
            }
            else {
                $PRTGRegion = Get-PRTGRegion -AWSRegion $Region
                $PRTGDeviceName = Get-PRTGDeviceName -CustomerName $CustomerName -PRTGRegion $PRTGRegion -Environment $EnvironmentType
                $PRTGGroupId = Get-PRTGGroupId -PRTGRegion $PRTGRegion
                $newPrtgDeviceNameParams = @{
                    Name             = $PRTGDeviceName
                    GroupId          = $PRTGGroupId
                    TemplateDeviceId = $PRTGTemplateDeviceId
                }
                $NewPRTGDevice = New-PRTGDevice @newPrtgDeviceNameParams
                [void]$( New-EC2Tag -Region $Region -ResourceId $awsEC2Instance.InstanceId -Tag @{ Key = "prtg-device-id"; Value = $NewPRTGDevice.Id } )
                $setPrtgDeviceParams = @{
                    Id         = $NewPRTGDevice.Id
                    IPAddress  = $awsEC2Instance.PrivateIpAddress
                    ServiceUrl = $URL
                    Resume     = $true
                }
                Set-PRTGDevice @setPrtgDeviceParams
                $setPrtgDeviceSensorParams = @{
                    DeviceId    = $NewPRTGDevice.Id
                    ServiceUrl  = $URL
                    UseDefaults = $true
                }
                Set-PRTGDeviceSensor @setPrtgDeviceSensorParams
                $kvStore.SetValue("IsMonitoringCompleted", $true)
            }
        }
        if (-not $SkipDuo) {
            if ($kvStore.GetValue("IsDuoAgentInstalled")) {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] The Duo agent has already been installed."
            }
            else {
                $installDuoAgentParams = @{
                    InstanceId = $awsEC2Instance.InstanceId
                    Region     = $Region
                }
                [void]$( Install-DuoAgent @installDuoAgentParams )
                $kvStore.SetValue("IsDuoAgentInstalled", $true)
            }
        }
        $newAWSEC2InstanceProfileParams = @{
            Region                 = $Region
            InstanceId             = $awsEC2Instance.InstanceId
            InstanceIamProfileName = $PersistentIamProfileName
        }
        Set-AWSEC2InstanceProfile @newAWSEC2InstanceProfileParams
        Write-LogInfo -Message "The deployment is complete. The instance can be accessed at $URL"
        $kvStore.SetValue("IsDeploymentCompleted", $true)
        Write-LogWarning -Message "##################################################################################################"
        Write-LogWarning -Message "Please speak to the DB team to get the Oracle DB configured before handing over to the customer"
        Write-LogWarning -Message "##################################################################################################"
    }
    finally {
        if (-not $SkipTranscript) {
            [void]$( Stop-Transcript )
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Transcript stopped."
        }
        if ($env:DTX_LOCAL_DOTMATICS_PATH_WAS_SET) {
            $env:DTX_LOCAL_DOTMATICS_PATH = $null
            $env:DTX_LOCAL_DOTMATICS_PATH_WAS_SET = $null
        }
    }
}
function New-DTXDiscoveryJsonFile {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param ()
    process {
        if ($PSCmdlet.ShouldProcess("$env:COMPUTERNAME", "Generate DTX Discovery JSON File")) {
            try {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Generating the DTX discovery JSON file..."
                Get-DTXSystem | Write-DTXSystem
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Successfully generated the DTX discovery JSON file."
            }
            catch {
                Write-LogCritical "[$( $MyInvocation.MyCommand )] Failed to generate the DTX discovery JSON file" -ThrowException -Exception $_
            }
        }
    }
}
function Remove-ScheduledTasks {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [string[]]
        [Parameter(Mandatory, Position = 0, ValueFromPipeline)]
        $Names
    )
    begin {
        $scheduled_task_obj = New-Object ScheduledTasks
    }
    process {
        if ($IsWindows) {
            try {
                foreach ($n in $Names) {
                    $mytask = $scheduled_task_obj.GetScheduledTask($n, $false)
                    if ($mytask -eq $null) {
                        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] The scheduled task: $n doesn't exist, skipping removing"
                        continue
                    }
                    if ($PSCmdlet.ShouldProcess("$n", "Removing Scheduled Task")) {
                        $scheduled_task_obj.RemoveScheduledTask($n)
                    }
                }
            }
            catch {
                Write-LogCritical -ThrowException -Message "[$( $MyInvocation.MyCommand )] Failed" -ThrowException -Exception $_
            }
        }
        else {
            Write-LogInfo "Removing scheduled tasks not supported on Linux."
        }
    }  
}
function Set-DTXAWSEC2IAMInstanceProfiles {
    [cmdletbinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]$RoleName,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateSet(
            "ap-northeast-1",
            "ap-southeast-1",
            "ap-southeast-2",
            "eu-central-1",
            "eu-west-1",
            "eu-west-2",
            "us-east-1",
            "us-west-1",
            "sa-east-1",
            "All",
            IgnoreCase = $true
        )]
        [Array]$Region,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidatePattern('^\d{12}$')]
        [String]$AWSAccountNumber,
        [switch]
        $SkipTranscript
    )
    If (-not $SkipTranscript) {
        $transcriptFile = "$( Get-Random ).txt"
        $transcriptFilePath = Join-Path -Path $([System.IO.Path]::GetTempPath() ) -ChildPath $transcriptFile
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Transcript started, output file is $transcriptFilePath"
        [void]$( Start-Transcript -Path $transcriptFilePath )
    }
    Confirm-AWSCredentials -Region "us-east-1" -AWSAccountNumber $AWSAccountNumber
    Confirm-AWSIAMRole -RoleName $RoleName -Region "us-east-1" -AWSAccountNumber $AWSAccountNumber
    if ($Region -contains "All") {
        $Region = [System.Collections.ArrayList]::new()
        $allActiveRegions = (Get-Defaults).AWS.ActiveRegions
        foreach ($r in $allActiveRegions) {
            $Region += $r.name
        }
    }
    try {
        foreach ($r in $Region) {
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Updating Region $r..."
            $ec2Instances = (Get-EC2Instance -Region $r -Filter @{Name = "instance-state-code"; Values = 0, 16, 32, 64, 80 }).Instances
            $setAlreadyCount = 0
            $newlySetCount = 0
            $otherRolecount = 0
            foreach ($instance in $ec2Instances) {
                if ($instance.IamInstanceProfile -eq $RoleName) {
                    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] EC2 Instance $( $instance.InstanceId ) already has $RoleName set..."
                    $setAlreadyCount += 1
                }
                elseif ($instance.IamInstanceProfile -eq $null) {
                    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Updating EC2 Instance $( $instance.InstanceId ) with IAM profile $RoleName..."
                    Register-EC2IamInstanceProfile -InstanceId $instance.InstanceId -IamInstanceProfile_Name $RoleName -Region $r
                    $newlySetCount += 1
                }
                else {
                    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] EC2 Instance $( $instance.InstanceId ) has IAM profile $( ($instance.IamInstanceProfile.Arn -split "instance-profile/")[1] ) set..."
                    $otherRolecount += 1
                }
            }
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] $r Standardized already set count: $setAlreadyCount"
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] $r Newly set count: $newlySetCount"
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] $r Different role set count: $otherRolecount"
        }
    }
    catch {
        Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] $( $_.Exception.Message )" -ThrowException -Exception $_
    }
    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] EC2 IAM Profile update has completed!"
}
function Set-DTXTomcatKeyStoreViaS3 {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$BucketName,
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$BucketRegion,
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$KeyStoreObjectKey,
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$KeyStoreFileHash,
        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [ValidateSet("SHA256", "SHA384", "SHA512")]
        [string]$KeyStoreFileHashAlgorithm = "SHA256",
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$CertificateFingerprint,
        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [ValidateSet("SHA256", "SHA384", "SHA512")]
        [string]$CertificateFingerprintHashAlgorithm = "SHA256",
        [switch]$AllowTomcatRestart
    )
    Begin {
        if (-not $IsWindows) { Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] This cmdlet should be executed from customer instances running Windows Server with Apache Tomcat installed." -ThrowException }
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] ################################################################################"
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] START: DOTMATICS - Tomcat Key Store Updater"
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] ################################################################################"
        function _Get-CurrentSSLCertificateFingerprint {
            return (Get-SSLCertificateFingerprint -Hostname "localhost" -Port 443 -HashAlgorithm $CertificateFingerprintHashAlgorithm)
        }
        function _Get-CurrentKeyStoreFileAndHash {
            $keyStoreFile = Get-TomcatKeyStoreFile -TomcatHomePath (Get-TomcatHomePath)
            $calculatedFileHash = (Get-FileHash -Path $keyStoreFile -Algorithm $KeyStoreFileHashAlgorithm).Hash
            return @{
                File = $keyStoreFile
                Hash = $calculatedFileHash
            }
        }
        function _Get-CandidateKeyStoreAndFileHash {
            $tempPath = [System.IO.Path]::GetTempPath()
            $keyStoreFile = Join-Path -Path $tempPath -ChildPath "$(Get-Random).jks"
            Get-S3Object -BucketName $BucketName -Region $BucketRegion -Key $KeyStoreObjectKey | Read-S3Object -File $keyStoreFile -Region $BucketRegion | Out-Null
            $calculatedFileHash = (Get-FileHash -Path $keyStoreFile -Algorithm $KeyStoreFileHashAlgorithm).Hash
            if ($calculatedFileHash -ine $KeyStoreFileHash) {
                Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] The calculated file hash ($calculatedFileHash) of the Tomcat key store file ($KeyStoreObjectKey) does not match the provided file hash ($KeyStoreFileHash)." -ThrowException
            }
            return @{
                File = $keyStoreFile
                Hash = $calculatedFileHash
            }
        }
        function _Test-CertFingerprintIsMatch {
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Testing if the current SSL certificate fingerprint matches the candidate SSL certificate fingerprint."
            $currentCertFingerprint = _Get-CurrentSSLCertificateFingerprint
            $candidateCertFingerprint = $CertificateFingerprint
            if ($currentCertFingerprint -ieq $candidateCertFingerprint) {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Match!"
                return $true
            }
            else {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] No match!"
                return $false
            }
        }
        function _Restart-TomcatService {
            if ($PSCmdlet.ShouldProcess("Tomcat Service", "Restart")) {
                $tomcatVersion = Get-TomcatVersion -TomcatHomePath (Get-TomcatHomePath)
                Restart-TomcatService -WaitAfterSeconds 30 -Version $TomcatVersion
                if (_Test-CertFingerprintIsMatch) {
                    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Certificate installed successfully. No further action needed."
                    return
                }
                Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] The new certificate was not installed after restarting the Tomcat service. The system needs to be investigated manually" -ThrowException
            }
            else {
                Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] WhatIf: Will be restarting the Tomcat service."
            }
        }
        function _Set-TomcatKeyStore {
            param (
                [string]$File,
                [string]$FileHash
            )
            $currentKeyStore = _Get-CurrentKeyStoreFileAndHash
            if ($currentKeyStore.Hash -ieq $FileHash) {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] The current Tomcat key store file hash matches the candidate Tomcat key store file hash."
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] However, the Tomcat service was never restarted after the key store file was replaced."
                return
            }
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] The current Tomcat key store file hash does not match the candidate Tomcat key store file hash."
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] The Tomcat key store file will be replaced."
            if ($PSCmdlet.ShouldProcess("Tomcat Key Store File", "Replace")) {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Creating a backup of the current keystore file..."
                $backupPath = $currentKeyStore.File + ".$(Get-Date -Format "yyyy-MM-dd-HH-mm").bak"
                Copy-Item -Path $currentKeyStore.File -Destination $backupPath -Force
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] The current keystore file has been backed up to $backupPath"
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Replacing the current keystore file with the candidate keystore file..."
                Copy-Item -Path $candidateKeyStore.File -Destination $currentKeyStore.File -Force
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] The Tomcat key store file has been replaced."
            }
            else {
                Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] WhatIf: Will be replacing $($currentKeyStore.File) with $($candidateKeyStore.File)"
            }
        }
    }
    Process {
        try {
            if (!(Test-IsBrowserSystem)) {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] No action is required, not a Browser system."
                return
            }
            if (_Test-CertFingerprintIsMatch) {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] No action is required, SSL certificate is correct."
                return
            }
            $candidateKeyStore = _Get-CandidateKeyStoreAndFileHash
            _Set-TomcatKeyStore -File $candidateKeyStore.File -FileHash $candidateKeyStore.Hash
            if ($AllowTomcatRestart) {
                _Restart-TomcatService
            }
            else {
                Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] The Tomcat service was not restarted because the -AllowTomcatRestart switch was not specified."
                Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] You need to manually restart the Tomcat service for the new certificate to be installed."
                return
            }
        }
        catch {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] $( $_.Exception.Message )" -ThrowException -Exception $_
        }
    }
    End {
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] ################################################################################"
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] END: DOTMATICS - Tomcat Key Store Updater"
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] ################################################################################"
    }
}
function Uninstall-DTXAutomoxAgent {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param()
    function Uninstall-AutomoxAgent {
        param(
            [Parameter(Mandatory = $true)]
            [System.Object]
            $App
        )
        if ($App.UninstallString -like "MsiExec.exe*/X*") {
            $arg = "/X $($app.PSChildName) /qn"
            Write-LogInfo "[$( $MyInvocation.MyCommand )] Uninstalling Automox Agent with arguments: $arg"
            return (Start-Process -FilePath "MsiExec.exe" -ArgumentList $arg -Wait -NoNewWindow -PassThru)
        }
        else {
            Write-LogInfo "[$( $MyInvocation.MyCommand )] Uninstalling Automox Agent with arguments: /SILENT and $($App.UninstallString)"
            return (Start-Process -FilePath $App.UninstallString -ArgumentList "/SILENT" -Wait -NoNewWindow -PassThru)
        }
    }
    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Running Uninstall-DTXAutomoxAgent function"
    if ($PSCmdlet.ShouldProcess("$env:COMPUTERNAME", "Uninstall Automox Agent")) {
        if (-not $IsWindows) {
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Uninstalling Automox not supported on OS's that are not Windows"
            return
        }
        $apps = Get-InstalledApps | Where-Object { $_.DisplayName -like "Automox*" }
        if (!$apps) {
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Automox Agent not found"
            return
        }
        Write-LogInfo "[$( $MyInvocation.MyCommand )] Found $($apps.Count) Automox Agent(s) installed on the machine"
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Uninstalling the Automox Agent"
        $retries = 0
        $automoxRemoved = $false
        do {
            if ($retries -gt 0) {
                $apps = Get-InstalledApps | Where-Object { $_.DisplayName -like "Automox*" }
            }
            $retries++
            $failedToRemove = 0
            foreach ($app in $apps) {
                $process = Uninstall-AutomoxAgent -App $app
                if ($process.ExitCode -ne 0) {
                    $process | Stop-Process -Force
                    $failedToRemove++
                }
            }
            if ($failedToRemove -eq 0) {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Automox Agent uninstalled successfully"
                $automoxRemoved = $true
                break
            }
        }
        while ($retries -lt 3)
        if (!$automoxRemoved) {
            Write-LogCritical -ThrowException -Message "[$( $MyInvocation.MyCommand )] Failed to uninstall Automox Agent."
        }
    }
}
function Uninstall-DTXSumoLogicAgent{
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [ValidateNotNullOrEmpty()]
        $SupportedWindowsVersions = @("2012", "2016", "2019", "2022"),
        [switch]
        $Force
    )
    function _Test-IsWindowsVersionSupported {
        $osVersion = (Get-SystemInfo).Windows.VersionAsYear
        if ($SupportedWindowsVersions -notcontains $osVersion) {
            return $false
        }
        return $true
    }    
    try {
        Show-Banner -AsLog -Message "Start -> Dotmatics - Summologic Agent Unininstall"
        if ($IsLinux -or $IsMacOS) {
            Write-LogWarning -Message "[$($MyInvocation.MyCommand)] This cmdlet is only supported on Windows operating systems. "
            return
        }
        if (-not (_Test-IsWindowsVersionSupported)) {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] The SumoLogic Agent is not supported on this version of Windows. Supported Windows versions are $($SupportedWindowsVersions -join ', ')." -ThrowException
        }
        $sumoLogicServiceName="sumo-collector"
        $sumologicUninstallFile = "C:\Program Files\Sumo Logic Collector\uninstall.exe"
        if (-not (Get-Service -ErrorAction SilentlyContinue | Where-Object { $_.Name -eq $sumoLogicServiceName})) {
            Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] The SumoLogic Agent service '$sumoLogicServiceName' does not exist on this machine, continuing the uninstall process to remove any previous installation files ..."
        }
        if ($PSCmdlet.ShouldProcess("$env:COMPUTERNAME", "Unininstall Sumologic Agent")) {
            Uninstall-SumoLogic  -ServiceName $sumoLogicServiceName -UninstallFile $sumologicUninstallFile
        }
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Sumologic agent uninstalled sucessfully."
    }
    finally {
        Show-Banner -AsLog -Message "Stop -> Dotmatics - SumoLogic Unininstall"
    }
}
function Write-DTXSystem {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateNotNullOrEmpty()]
        [PSObject]
        $InputObject
    )
    Begin {
        if ($InputObject.Length -gt 1) {
            Write-LogCritical -ThrowException -Message "[$( $MyInvocation.MyCommand )] Write-DTXSystem only accepts one pipeline input"
        }
        $rootPath = (Get-Location).Drive.Root
        $rootJsonPath = Join-Path $rootPath "dtx_discovery.json"
        if (Test-Path $rootJsonPath) {
            Remove-Item -Path $rootJsonPath -Force
        }
    }
    Process {
        ForEach ($input in $InputObject) {
            $input | ConvertTo-Json -Depth 100 -EnumsAsStrings | Out-File -Path (Get-DTXLocalDiscoveryFilePath) -Force
        }
    }
} 
function Write-JsonCache {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateNotNullOrEmpty()]
        [PSObject]
        $InputObject,
        [Parameter(Mandatory = $true, ValueFromPipeline = $false)]
        [ValidateNotNullOrEmpty()]
        [PSObject]
        $JsonFileName
    )
    Begin {
        $jsonFilePath = Join-Path (Get-LocalDotmaticsPath) $JsonFileName
    }
    Process {
        ForEach ($input in $InputObject) {
            $input | ConvertTo-Json -Depth 100 -EnumsAsStrings | Out-File -Path $jsonFilePath -Force
        }
    }
}