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.Exception] } {
                    throw [DTXPSException]::new($_message, $_.ScriptStackTrace)
                }
                { $_ -is [System.Management.Automation.ErrorRecord] } {
                    throw [DTXPSException]::new($_message, $_.ScriptStackTrace, $_.Exception)
                }
                default {
                    throw [DTXPSException]::new($_message)
                }
            }
        }
    }
    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)
    }
}
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 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"))
    }
}
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 DTXTomcatServerSettings {
    hidden [string]$className = $this.GetType().Name
    hidden [System.Collections.Hashtable] $dtxSystemInfo
    hidden [Hashtable] $dtxDefaults
    hidden [boolean] $isChangeRequired = $false
    hidden [int] $settingsVersion
    hidden [hashtable] $desiredTomcatServerSettings
    DTXTomcatServerSettings (
        [System.Collections.Hashtable]$DTXSystemInfo,
        [Hashtable] $DTXDefaults,
        [int] $SettingsVersion
    ) {
        $AllowedVersions = @(1)
        if ($AllowedVersions -notcontains $SettingsVersion) {
            [DTXLogger]::LogCriticalAndThrow("[$($this.className)] The server settings version $SettingsVersion is not supported.")
        }
        $this.dtxSystemInfo = $DTXSystemInfo
        $this.dtxDefaults = $DTXDefaults
        $this.settingsVersion = $SettingsVersion
    }
    hidden [hashtable] GetDesiredTomcatServerSettings(
        [int] $SettingsVersion
    ) {
        if ($this.desiredTomcatServerSettings.Keys.Count -gt 0) {
            return $this.desiredTomcatServerSettings
        }
        $_desiredTomcatServerSettings = $this.dtxDefaults.Tomcat.ServerSettings | Where-Object { $_.Version -eq $SettingsVersion }
        if ($_desiredTomcatServerSettings.Count -eq 0) {
            [DTXLogger]::LogCriticalAndThrow("[$($this.className)] No server settings found for the current server settings version.")
        }
        $tomcatVersion = $this.dtxSystemInfo.AppInfo.Tomcat.Version
        $javaVersion = $this.dtxSystemInfo.AppInfo.Tomcat.Java.Version
        [DTXLogger]::LogInfo("[$($this.className)] Detected Apache Tomcat version: $tomcatVersion")
        [DTXLogger]::LogInfo("[$($this.className)] Detected Java version: $javaVersion")
        if ([System.Version] $_desiredTomcatServerSettings.JavaMinVersion -gt [System.Version] $javaVersion) {
            [DTXLogger]::LogCriticalAndThrow("[$($this.className)] The Java version ($javaVersion) is not supported by the server settings.")
        }
        if ([System.Version] $_desiredTomcatServerSettings.TomcatMinVersion -gt [System.Version] $tomcatVersion) {
            [DTXLogger]::LogCriticalAndThrow("[$($this.className)] The Tomcat version ($tomcatVersion) is not supported by the server settings.")
        }
        $this.desiredTomcatServerSettings = $_desiredTomcatServerSettings
        return $this.desiredTomcatServerSettings
    }
    hidden [xml] GetTomcatServerXmlContents() {
        $serverXmlPath = $this.dtxSystemInfo.AppInfo.Tomcat.ServerXML.Path
        $result = $null
        try {
            $result = [xml](Get-Content -Path $serverXmlPath -ErrorAction Stop)
        }
        catch {
            [DTXLogger]::LogCriticalAndThrow("[$($this.className)] Unable to read the server.xml file at $serverXmlPath", $_)
        }
        return $result
    }
    hidden [bool] IsTomcatServerXmlConnectorChangeRequired(
        [xml] $TomcatServerXmlContents,
        [hashtable] $DesiredTomcatServerSettings
    ) {
        [DTXLogger]::LogDebug("[$($this.className)] Checking if a change is required for Tomcat server.xml connector settings...")
        $changeRequired = $false
        foreach ($connector in $TomcatServerXmlContents.Server.Service.Connector) {
            [DTXLogger]::LogDebug("[$($this.className)] Checking connector: $($connector.protocol)")
            if ((($connector.protocol -like "*HTTP/1.1*") -or ($connector.protocol -like "*Http11NioProtocol*")) -and $connector.SSLEnabled -eq "true") {
                if (!([DTXXmlUtils]::CompareXmlAttribute($connector, "enableLookups", $DesiredTomcatServerSettings.EnableLookups))) {
                    [DTXLogger]::LogDebug("[$($this.className)] Change required for enableLookups on connector. Expected: $($DesiredTomcatServerSettings.EnableLookups), Actual: $($connector.enableLookups)")
                    $changeRequired = $true
                }
                if (!([DTXXmlUtils]::CompareXmlAttribute($connector, "sslEnabledProtocols", ($DesiredTomcatServerSettings.SSLSettings.Protocols -join ",")))) {
                    [DTXLogger]::LogDebug("[$($this.className)] Change required for sslEnabledProtocols on connector. Expected: $($DesiredTomcatServerSettings.SSLSettings.Protocols), Actual: $($connector.sslEnabledProtocols)")
                    $changeRequired = $true
                }
                if (!([DTXXmlUtils]::CompareXmlAttribute($connector, "ciphers", $DesiredTomcatServerSettings.SSLSettings.Ciphers))) {
                    [DTXLogger]::LogDebug("[$($this.className)] Change required for ciphers on connector. Expected: $($DesiredTomcatServerSettings.SSLSettings.Ciphers), Actual: $($connector.ciphers)")
                    $changeRequired = $true
                }
                $http2 = $connector.SelectSingleNode("UpgradeProtocol[@className='org.apache.coyote.http2.Http2Protocol']")
                if (-not $http2 -and $DesiredTomcatServerSettings.EnableHTTP2) {
                    [DTXLogger]::LogDebug("[$($this.className)] Change required for HTTP2 on connector. Expected: $($DesiredTomcatServerSettings.EnableHTTP2), Actual: Disabled")
                    $changeRequired = $true
                }
                if ($http2 -and -not $DesiredTomcatServerSettings.EnableHTTP2) {
                    [DTXLogger]::LogDebug("[$($this.className)] Change required for HTTP2 on connector. Expected: $($DesiredTomcatServerSettings.EnableHTTP2), Actual: Enabled")
                    $changeRequired = $true
                }
            }
        }
        return $changeRequired
    }
    hidden [bool] IsTomcatServerXmlServerInfoChangeRequired(
        [xml] $TomcatServerXmlContents,
        [hashtable] $DesiredTomcatServerSettings
    ) {
        [DTXLogger]::LogDebug("[$($this.className)] Checking if a change is required for Tomcat server.xml server info settings...")
        $changeRequired = $false
        $errorReportValve = $TomcatServerXmlContents.Server.Service.Engine.Host.SelectSingleNode("Valve[@className='org.apache.catalina.valves.ErrorReportValve']")
        if ($errorReportValve) {       
            if (!([DTXXmlUtils]::CompareXmlAttribute($errorReportValve, "showServerInfo", $DesiredTomcatServerSettings.ShowServerInfo))) {
                [DTXLogger]::LogDebug("[$($this.className)] Change required for showServerInfo on ErrorReportValve. Expected: $($DesiredTomcatServerSettings.ShowServerInfo), Actual: $($errorReportValve.showServerInfo)")
                $changeRequired = $true
            }
        }
        else {
            [DTXLogger]::LogDebug("[$($this.className)] Change required for showServerInfo on ErrorReportValve. Expected: $($DesiredTomcatServerSettings.ShowServerInfo), Actual: Not found")
            $changeRequired = $true
        }
        return $changeRequired
    }
    hidden [xml] SetTomcatServerXmlConnectorSettings(
        [xml] $TomcatServerXmlContents,
        [hashtable] $DesiredTomcatServerSettings
    ) {
        $connectorsUpdated = $false
        foreach ($connector in $TomcatServerXmlContents.Server.Service.Connector) {
            if ((($connector.protocol -like "*HTTP/1.1*") -or ($connector.protocol -like "*Http11NioProtocol*")) -and $connector.SSLEnabled -eq "true") {
                [DTXXmlUtils]::UpdateXmlAttribute($connector, "enableLookups", $DesiredTomcatServerSettings.EnableLookups)
                [DTXXmlUtils]::UpdateXmlAttribute($connector, "sslEnabledProtocols", ($DesiredTomcatServerSettings.SSLSettings.Protocols -join ","))
                [DTXXmlUtils]::UpdateXmlAttribute($connector, "ciphers", $DesiredTomcatServerSettings.SSLSettings.Ciphers)
                $http2 = $connector.SelectSingleNode("UpgradeProtocol[@className='org.apache.coyote.http2.Http2Protocol']")
                if ($DesiredTomcatServerSettings.EnableHTTP2) {
                    if (-not $http2) {
                        $newHttp2 = $TomcatServerXmlContents.CreateElement("UpgradeProtocol")
                        $newHttp2.SetAttribute("className", "org.apache.coyote.http2.Http2Protocol")
                        $connector.AppendChild($newHttp2) | Out-Null
                    }
                }
                else {
                    if ($http2) {
                        $connector.RemoveChild($http2) | Out-Null
                    }
                }
                $connectorsUpdated = $true
            }
        }
        if (-not $connectorsUpdated) {
            [DTXLogger]::LogCriticalAndThrow("[$($this.className)] Unable to find any HTTP connectors with SSL enabled. Please make sure the Tomcat server XML file is configured correctly.")
        }
        return $TomcatServerXmlContents
    }
    hidden [xml] SetTomcatServerXmlServerInfoSettings(
        [xml] $TomcatServerXmlContents,
        [hashtable] $DesiredTomcatServerSettings
    ) {
        $errorReportValve = $TomcatServerXmlContents.Server.Service.Engine.Host.SelectSingleNode("Valve[@className='org.apache.catalina.valves.ErrorReportValve']")
        if ($errorReportValve) {
            [DTXXmlUtils]::UpdateXmlAttribute($errorReportValve, "showServerInfo", $DesiredTomcatServerSettings.ShowServerInfo)
        }
        else {
            $newErrorReportValve = $TomcatServerXmlContents.CreateElement("Valve")
            $newErrorReportValve.SetAttribute("className", "org.apache.catalina.valves.ErrorReportValve")
            $newErrorReportValve.SetAttribute("showReport", $true)
            $newErrorReportValve.SetAttribute("showServerInfo", $DesiredTomcatServerSettings.ShowServerInfo)
            $TomcatServerXmlContents.Server.Service.Engine.Host.AppendChild($newErrorReportValve) | Out-Null
        }
        return $TomcatServerXmlContents
    }
    hidden [xml] CleanUpTomcatServerXml(
        [xml] $TomcatServerXmlContents
    ) {
        $serviceXmlNode = $TomcatServerXmlContents.Server.Service
        $http2OnServiceXmlNode = $serviceXmlNode.SelectSingleNode("UpgradeProtocol[@className='org.apache.coyote.http2.Http2Protocol']")
        if ($http2OnServiceXmlNode -and ($http2OnServiceXmlNode.ParentNode -eq $serviceXmlNode)) {
            $serviceXmlNode.RemoveChild($http2OnServiceXmlNode) | Out-Null
        }
        return $TomcatServerXmlContents
    }
    [bool] ChangeRequired() {
        try {
            $this.isChangeRequired = $false
            [DTXLogger]::LogDebug("[$($this.className)] Checking if a change is required for Tomcat server settings...")
            $_desiredTomcatServerSettings = $this.GetDesiredTomcatServerSettings($this.settingsVersion)
            $_tomcatServerXmlContents = $this.GetTomcatServerXmlContents()
            if ($this.IsTomcatServerXmlConnectorChangeRequired($_tomcatServerXmlContents, $_desiredTomcatServerSettings)) {
                [DTXLogger]::LogDebug("[$($this.className)] Change required for Tomcat server.xml connector settings.")
                $this.isChangeRequired = $true
            }
            if ($this.IsTomcatServerXmlServerInfoChangeRequired($_tomcatServerXmlContents, $_desiredTomcatServerSettings)) {
                [DTXLogger]::LogDebug("[$($this.className)] Change required for Tomcat server.xml server info settings.")
                $this.isChangeRequired = $true
            }
        }
        catch {
            [DTXLogger]::LogCriticalAndThrow("[$($this.className)] An error occurred while checking if a change is required.", $_)
        }
        [DTXLogger]::LogDebug("[$($this.className)] Change required: $($this.isChangeRequired)")
        return $this.isChangeRequired
    }
    [void] PerformChange() {
        if ($this.isChangeRequired) {
            [DTXLogger]::LogInfo("[$($this.className)] Performing the change for Tomcat server settings...")
            try {
                $_desiredTomcatServerSettings = $this.GetDesiredTomcatServerSettings($this.settingsVersion)
                $_tomcatServerXmlContents = $this.GetTomcatServerXmlContents()
                $updatedServerXmlContents = $this.SetTomcatServerXmlConnectorSettings($_tomcatServerXmlContents, $_desiredTomcatServerSettings)
                $updatedServerXmlContents = $this.SetTomcatServerXmlServerInfoSettings($updatedServerXmlContents, $_desiredTomcatServerSettings)
                $updatedServerXmlContents = $this.CleanUpTomcatServerXml($updatedServerXmlContents)
                $formattedXml = [DTXXmlUtils]::FormatXml($updatedServerXmlContents)
                if ([DTXXmlUtils]::TestXmlContentIsValid($formattedXml) -eq $false) {
                    [DTXLogger]::LogCriticalAndThrow("[$($this.className)] The formatted server.xml content is not valid XML.")
                }
                [DTXUtils]::WriteStringToFileWithRetry($formattedXml, $this.dtxSystemInfo.AppInfo.Tomcat.ServerXML.Path)
                [DTXLogger]::LogInfo("[$($this.className)] The change has been successfully applied.")
            }
            catch {
                [DTXLogger]::LogCriticalAndThrow("[$($this.className)] An error occurred while performing the change.", $_)
            }
        }
        else {
            [DTXLogger]::LogInfo("[$($this.className)] No change required, not performing any action.")
        }
    }
    [void] TestAfterChange() {
        if ($this.isChangeRequired) {
            [DTXLogger]::LogInfo("[$($this.className)] Testing Tomcat server settings after the change...")
            try {
                $_desiredTomcatServerSettings = $this.GetDesiredTomcatServerSettings($this.settingsVersion)
                $_tomcatServerXmlContents = $this.GetTomcatServerXmlContents()
                if ($this.IsTomcatServerXmlConnectorChangeRequired($_tomcatServerXmlContents, $_desiredTomcatServerSettings)) {
                    [DTXLogger]::LogCriticalAndThrow("[$($this.className)] The Tomcat server.xml connector settings are not as expected after the change.")
                }
                if ($this.IsTomcatServerXmlServerInfoChangeRequired($_tomcatServerXmlContents, $_desiredTomcatServerSettings)) {
                    [DTXLogger]::LogCriticalAndThrow("[$($this.className)] The Tomcat server.xml server info settings are not as expected after the change.")
                }
                [DTXLogger]::LogInfo("[$($this.className)] The Tomcat server settings are as expected after the change.")
            }
            catch {
                [DTXLogger]::LogCriticalAndThrow("[$($this.className)] An error occurred while testing the Tomcat server settings after the change.", $_)
            }
        }
        else {
            [DTXLogger]::LogInfo("[$($this.className)] No change required, not performing any action.")
        }
    }
}
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-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()
    $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-Defaults {
    if ($env:DTX_DEFAULTS_JSON_FILE_PATH) {
        if (Test-Path -Path $env:DTX_DEFAULTS_JSON_FILE_PATH) {
            return Get-Content -Path $env:DTX_DEFAULTS_JSON_FILE_PATH | ConvertFrom-Json -Depth 100  -AsHashtable
        }
        else {
            throw [System.IO.FileNotFoundException] "File $($env:DTX_DEFAULTS_JSON_FILE_PATH) not found."
        }
    }
    return Get-Content -Path $(Get-ExternalFilePath -FileName "defaults.json") | 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-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-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-LocalDotmaticsPath {
    return [DTXPathProvider]::GetLocalDotmaticsPath()
}
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-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-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-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 = 'SHA256'
    )
    try {
        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 
            }
        )
        $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()
        }
    }
    return $fingerprint
}
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
        TomcatServerXmlPath = $null
        TomcatJavaOptions   = $null
        TomcatJavaVersion   = $null
        TomcatJavaVendor    = $null
        TomcatJavaHomePath  = $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.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 {
        $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-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 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-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 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-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 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"
                    }
                    Path        = "UNKNOWN"
                    WebAppsPath = "UNKNOWN"
                    Version     = "UNKNOWN"
                    Java        = @{
                        Version = "UNKNOWN"
                        Vendor  = "UNKNOWN"
                        Path    = "UNKNOWN"
                    }
                    ServerXML   = @{
                        Path         = "UNKNOWN"
                        Port         = "UNKNOWN"
                        HttpProtocol = "UNKNOWN"
                        SSLProtocols = "UNKNOWN"
                        SSLCiphers   = "UNKNOWN"
                        KeyStoreFile = "UNKNOWN"
                        WebAppBase   = "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
                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.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
                $serverXml = $meta.TomcatServerXmlPath ?? $returnObj.AppInfo.Tomcat.ServerXML.Path
                if (Test-Path $serverXml) {
                    $returnObj.AppInfo.Tomcat.ServerXML.Path = $serverXml
                    try {
                        [xml]$xmlfile = Get-Content $serverXml -ErrorAction Stop
                        $returnObj.AppInfo.Tomcat.ServerXML.Port = $xmlfile["Server"]["Service"]["Connector"].port
                        $returnObj.AppInfo.Tomcat.ServerXML.HttpProtocol = $xmlfile["Server"]["Service"]["Connector"].protocol
                        $returnObj.AppInfo.Tomcat.ServerXML.SSLProtocols = $xmlfile["Server"]["Service"]["Connector"].sslEnabledProtocols
                        $returnObj.AppInfo.Tomcat.ServerXML.SSLCiphers = $xmlfile["Server"]["Service"]["Connector"].ciphers
                        $returnObj.AppInfo.Tomcat.ServerXML.KeyStoreFile = $xmlfile["Server"]["Service"]["Connector"].keystoreFile
                        $returnObj.AppInfo.Tomcat.ServerXML.WebAppBase = $xmlfile["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-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()
    process {
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Starting Tomcat Changes..."
        $dtxDefaults = Get-Defaults
        $dtxSysData = Get-DTXSystem
        if ($dtxSysData.ComputedQueries.IsWebServer.Status -eq $true -and $dtxSysData.ComputedQueries.IsWebServer.AppName -eq "Tomcat") {
            $tomcatChangeList = @(
                [DTXTomcatServerSettings]::new($dtxSysData, $dtxDefaults, 1)
            )
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Tomcat change list: $($tomcatChangeList.Count)"
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Tomcat change list names: $($tomcatChangeList.GetType().Name)"
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Checking if change is required..."
            $changeRequired = $false
            foreach ($item in $tomcatChangeList) {
                if ($item.ChangeRequired()) {
                    $changeRequired = $true
                }
            }
            if ($changeRequired) {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Change is required."
                $tomcatServiceName = $dtxSysData.AppInfo.Tomcat.Service.Name
                try {
                    if ($PSCmdlet.ShouldProcess($tomcatServiceName, "Stop")) {
                        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Stopping Tomcat Service: $($tomcatServiceName)"
                        Stop-ServiceWithRetry -Name $tomcatServiceName -Retries 10 -WaitSeconds 15
                    }
                    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Performing changes since Tomcat has been stopped"
                    foreach ($item in $tomcatChangeList) {
                        if ($PSCmdlet.ShouldProcess($item.GetType().Name, "Performed Tomcat change")) {
                            $item.PerformChange()
                        }
                    }
                }
                finally {
                    if ($PSCmdlet.ShouldProcess($tomcatServiceName, "Start")) {
                        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Starting Tomcat service..."
                        Start-Service -Name $tomcatServiceName
                    }
                }
                $sleepTime = 60
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Sleeping for $sleepTime seconds to allow Tomcat to start fully..."
                Start-Sleep -Seconds $sleepTime
                foreach ($item in $tomcatChangeList) {
                    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Performing testing for $($item.GetType().Name)"
                    $item.TestAfterChange()
                }
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Finished Performing Tomcat changes."
            }
            else {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] No changes required."
            }
        }
        else {
            write-LogInfo  -Message "[$( $MyInvocation.MyCommand )] Not a Tomcat Webserver. No changes required."
        }
    }
}
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
        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
        }
        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
        }
    }
}