DTX.Cloud.Management.psm1

class KeyValueStore {
    hidden [string]$Path
    KeyValueStore(
        [string]$Path
    ) {
        $this.Path = $Path
        if (-not (Test-Path -Path $this.Path)) {
            New-Item -Path $this.Path -ItemType File -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
        New-Item -Path $this.Path -ItemType File -Force
    }
    [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 "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
     }
}
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 )"
            throw $_
        }
    }
}
function Assert-True {
  [CmdletBinding()]
  param(
    [Parameter(Position = 0)]
    [object]
    $Condition,
    [Parameter(Position = 1)]
    [string]
    $Message
  )
  Set-StrictMode -Version 'Latest'
  if ( -not $condition ) {
    throw  "[$( $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
        $backupFilePath = Join-Path $gifCachePath "toolkit.bak.$(Get-Date -Format "yyyy-MM-dd_hh-mm-ss").zip"
        Compress-Archive -CompressionLevel NoCompression -Path $toolkitPath -DestinationPath "toolkit.bak.$(Get-Date -Format "yyyy-MM-dd_hh-mm-ss").zip"
        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."
                throw
            }
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] $( $_.Exception.Message )"
            throw
        }
    }
}
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 )"
            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 )"
            throw
        }
    }
}
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 )"
            throw
        }
    }
}
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 )"
            throw
        }
    }
}
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 )"
            throw
        }
    }
}
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 )"
            throw
        }
    }
}
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 )"
        }
    }
}
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"
            throw
        }
        catch [System.UriFormatException] {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] Invalid hostname: This may be caused by whitespace or the server name is too long"
            throw
        }
        catch [System.Net.Http.HttpRequestException] {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] PRTG username and/or password are incorrect. Please try again."
            throw
        }
        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?"
            throw
        }
        catch {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] $( $_.Exception.Message )"
            throw
        }
    }
}
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 = 60
    )
    $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) {
        throw "Failed to copy $Path to $Destination after $retryCount attempts."
    }
}
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 )"
            throw
        }
    }
}
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 )"
            throw
        }
    }
}
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 )"
            throw
        }
    }
}
function Get-BrowserPlatformPath {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [PSObject]
        $TomcatWebAppPath
    )
    return Join-Path $TomcatWebAppPath  "browser"
}
function Get-BrowserPlatformPropertiesFilePath {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [PSObject]
        $TomcatWebAppPath
    )
    return Join-Path  (Get-BrowserPlatformPath -TomcatWebAppPath $TomcatWebAppPath)  "WEB-INF" "browser.properties"
}
function Get-BrowserPlatformGifcachePath {
    $thePath = Join-Path (Get-TomcatHomePath) "webapps" "browser" "gifcache" 
    if (!(Test-Path $thePath)) {
        throw "[$( $MyInvocation.MyCommand )] Unable to locate the Browser Platform Gifcache directory. The path $thePath does not exist."
    }
    return $thePath
}
function Get-BrowserPlatformGifcacheToolkitPath {
    $thePath = Join-Path (Get-BrowserPlatformGifcachePath) "toolkit"
    if (!(Test-Path $thePath)) {
        throw "[$( $MyInvocation.MyCommand )] Unable to locate the Browser Platform Gifcache Toolkit directory. The path $thePath does not exist."
    }
    return $thePath
}
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 {
    function Get-DefaultsFromS3 {
        $localCachePath = (Join-Path $HOME ".DTX.Cloud.Management")
        $localStateFile = Join-Path $localCachePath "GetDefaultsState.json"
        $myModule = $MyInvocation.MyCommand.ScriptBlock.Module
        $myRealVersion = $myModule.Version.ToString()
        $branchName = $myModule.PrivateData.BranchName
        $folder = "production"
        if ($branchName -ne "master") {
            $folder = "development"
            $myRealVersion += "-" + $myModule.PrivateData.PSData.Prerelease
        }
        $s3FileName = "bin/$folder/dot_po_cloudops_modules/$branchName/$myRealVersion/defaults.json"
        if (Test-Path $localStateFile) {
            $myStateData = Get-Content -Path $localStateFile | ConvertFrom-Json -Depth 100
            if ($myStateData.Key -eq $s3FileName) {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Returning existing defaults file"
                try {
                    return Get-Content -Path $myStateData.File | ConvertFrom-Json -Depth 100
                }
                catch {
                    Remove-Item $localStateFile -Force
                    Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Failed to load json from existing file, falling through to get a new file"
                }
            }
        }
        $defaultRegion = Get-DefaultAWSRegion
        if ($null -eq $defaultRegion) {
            $defaultRegion = "eu-west-1"
        }
        else {
            $defaultRegion = $defaultRegion.Region
        }
        $s3BucketName = (Get-SSMParameterValue -Name "/cloud/public/regional/s3/v1/buckets/cf_extensions/name" -Region $defaultRegion -WithDecryption:$true -ErrorAction Stop).Parameters[0].Value
        Remove-Item -Path "$localCachePath/*" -Recurse -Force -EA SilentlyContinue
        $defaultsFile = Get-FileFromS3 -BucketName $s3BucketName -BucketKey $s3FileName -DirectoryPath $localCachePath
        $myState = @{
            Key  = $s3FileName
            File = $defaultsFile
        }
        ConvertTo-Json $myState | Out-File -FilePath (New-Item $localStateFile -Force)
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Returning new defaults file"
        return Get-Content -Path $myState.File | ConvertFrom-Json -Depth 100
    }
    $localPath = "$PSScriptRoot/../ExternalFiles/defaults.json"
    if (Test-Path $localPath) {
        return Get-Content -Path $localPath | ConvertFrom-Json -Depth 100
    }
    else {
        return Get-DefaultsFromS3
    }
}
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 "[$( $MyInvocation.MyCommand )] Tag key: $Key not found"
}
function Get-FileFromS3 {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $BucketName,
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $BucketKey,
        [Parameter(Mandatory = $false)]
        [string]
        $DirectoryPath,
        [switch]
        $PreserveFileName
    )
    $ProgressPreference = 'SilentlyContinue'
    if (!$DirectoryPath) {
        $DirectoryPath = [System.IO.Path]::GetTempPath()
        if (!$DirectoryPath) {
            throw "[$( $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
    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Downloading file '$BucketKey' from bucket '$BucketName' in region '$bucketRegion' to file path: $path"
    try {
        $params = @{
            BucketName = $BucketName
            Key        = $BucketKey
            File       = $path
        }
        if ($bucketRegion) {
            $params.Region = $bucketRegion
        }
        Read-S3Object @params -ErrorAction Stop | Out-Null
    }
    catch {
        throw "[$( $MyInvocation.MyCommand )] Failed to download file '$BucketKey' from bucket '$BucketName' in region '$bucketRegion'. $_"
    }
    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] File downloaded to $path"
    return $path
}
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 {
        throw "[$( $MyInvocation.MyCommand )] This platform is not supported yet."
    }
}
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]) {
        throw "Multiple JChem Cartridge processes found. This is not expected."
    }
    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 "Unable to find the JChem Cartridge process."
    }
    try {
        $returnObj.Cartridge.Service = Get-JChemCartridgeService
    }
    catch {
        Write-LogWarning -Message "Unable to find the JChem Cartridge service."
    }
    try {
        $returnObj.Cartridge.HomePath = Get-JChemCartridgeHomePath
    }
    catch {
        Write-LogWarning -Message "Unable to find the JChem Cartridge home path."
    }
    try {
        $returnObj.Cartridge.Version = Get-JChemCartridgeVersion
    }
    catch {
        Write-LogWarning -Message "Unable to determine the JChem Cartridge version."
    }
    return $returnObj
}
function Get-LocalDiscoveryFilePath {
    return Join-Path (Get-LocalDotmaticsPath) "dtx_discovery.json"
}
function Get-LocalDotmaticsPath {
    if ($IsWindows) {
        $dirPath = Join-Path $env:PROGRAMDATA "dotmatics"
        if (!(Test-Path $dirPath)) {
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] $dirPath does not exist. Creating it..."
            New-Item -Path $dirPath -ItemType Directory -Force | Out-Null
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Created."
        }
        return $dirPath
    }
    if ($IsLinux -or $IsMacOS) {
        $dirPath = Join-Path "/" "var" "local" "dotmatics"
        if (!(Test-Path $dirPath)) {
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] $dirPath does not exist. Creating it..."
            New-Item -Path $dirPath -ItemType Directory -Force | Out-Null
            Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Created."
        }
        return $dirPath
    }
    Write-LogCritical -ThrowException -Message "[$( $MyInvocation.MyCommand )] Only Windows, Linux and MacOS operating systems are supported."
}
function Get-LocalStateFilePath {
    return Join-Path (Get-LocalDotmaticsPath) "dtx_state.json"
}
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 = @{
        Mol2ImgVersion        = $null
        Mol2ImgChecksum       = $null
        Mol2NameBatchChecksum = $null
    }
    if (!$IsWindows -and $IsLinux) {
        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Only Windows and Linux operating systems are supported."
        return $returnObj
    }
    $gifcacheToolkitPath = Get-BrowserPlatformGifcacheToolkitPath
    if ($IsWindows) {
        $mol2ImgFilePath = Join-Path $gifcacheToolkitPath "mol2img.exe"
        $mol2NameBatchFilePath = Join-Path $gifcacheToolkitPath "mol2name_batch.exe"
    }
    if ($IsLinux) {
        $mol2ImgFilePath = Join-Path $gifcacheToolkitPath "mol2img"
        $mol2NameBatchFilePath = Join-Path $gifcacheToolkitPath "mol2name_batch"
    }
    try {
        if (Test-Path $mol2ImgFilePath) {
            $returnObj.Mol2ImgVersion = [string](& $mol2ImgFilePath "-version")
            $returnObj.Mol2ImgChecksum = (Get-FileHash -Path $mol2ImgFilePath -Algorithm MD5).Hash
        }
        else {
            Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Unable to find the the mol2img binary."
        }
    }
    catch {
        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Unable to find the the OpenEye Mol2Img version."
    }
    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."
    }
    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]) {
        throw "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]) {
        throw "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 "Unable to find the Oracle Database process."
        }
        try {
            $returnObj.Database.Service = Get-OracleDatabaseService
        }
        catch {
            Write-LogWarning -Message "Unable to find the Oracle Database service."
        }
        try {
            $returnObj.Database.HomePath = Get-OracleDatabaseHomePath
        }
        catch {
            Write-LogWarning -Message "Unable to find the Oracle Database home path."
        }
        try {
            $returnObj.Database.Version = Get-OracleDatabaseVersion
        }
        catch {
            Write-LogWarning -Message "Unable to determine the Oracle Database version."
        }
    }
    Get-OracleDatabaseMetadata
    function Get-OracleDatabaseListenerMetadata {
        try {
            $returnObj.Listener.Process = Get-OracleDatabaseListenerProcess
        }
        catch {
            Write-LogWarning -Message "Unable to find the Oracle Database Listener process."
        }
        try {
            $returnObj.Listener.Service = Get-OracleDatabaseListenerService
        }
        catch {
            Write-LogWarning -Message "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 )"
            throw
        }
    }
}
function Get-PRTGRegion {
    [cmdletbinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]$AWSRegion
    )
    process {
        $regions = (Get-Defaults).PRTG.RegionMapping.PSObject.Properties | foreach -begin { $ht = @{ } } -process { $ht[$_.Name] = $_.Value } -end { $ht }
        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 {
        throw $_.Exception.Message
    }
    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 "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]) {
        throw "Multiple Tomcat services found and set to automatic start. Please ensure only one Tomcat service is installed or set to start automatically."
    }
    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 "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 "Unable to locate the Java executable"
    }
    $javaVersionOutput = & $javaExec -version 2>&1
    if (-not $javaVersionOutput) {
        Write-LogCritical -ThrowException -Message "Unable to determine the Java version"
    }
    $javaVersionOutput = $javaVersionOutput -split "`r`n"
    if ($javaVersionOutput.Count -ne 3) {
        Write-LogCritical -ThrowException -Message "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 "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 "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)) {
        throw "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)) {
        throw "Unable to find key store file at $fullkeyStorePath"
    }
    $tomcatKeyStoreName = [string](Get-Defaults).Tomcat.KeyStoreName
    if ([System.IO.Path]::GetFileName($fullkeyStorePath) -ne $tomcatKeyStoreName) {
        throw "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 "Unable to determine the Tomcat home path"
    }
    try {
        $returnObj.TomcatVersion = Get-TomcatVersionV2
    }
    catch {
        Write-LogWarning -Message "Unable to determine the Tomcat version"
    }
    try {
        $returnObj.TomcatProcess = Get-TomcatProcess
    }
    catch {
        Write-LogWarning -Message "Unable to determine the Tomcat process"
    }
    try {
        $returnObj.TomcatService = Get-TomcatService
    }
    catch {
        Write-LogWarning -Message "Unable to determine the Tomcat service"
    }
    try {
        $returnObj.TomcatServerXmlPath = Get-TomcatServerXMLPath
    }
    catch {
        Write-LogWarning -Message "Unable to determine the Tomcat server.xml path"
    }
    try {
        $returnObj.TomcatJavaOptions = Get-TomcatJavaOptions
    }
    catch {
        Write-LogWarning -Message "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 "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]) {
        throw "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*") {
        throw "Invalid Tomcat home path."
    }
    $version = ($TomcatHomePath -split "tomcat" | Select-Object -Last 1).Trim()
    $version = $version[0]
    if (-not $version) {
        throw "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 )"
            throw $_
        }
    }
}
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 "This cmdlet is only supported on Windows."
        }
        if (-not (Test-Path -Path $InstallationFile)) {
                throw "[$( $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 "[$( $MyInvocation.MyCommand )] Error installing CrowdStrike Agent. Exit code: $($installResult.ExitCode)."
        }
        Start-Sleep -Seconds 5
        Start-Service -Name $ServiceName
        if (-not (Test-IsServiceRunning -SearchTerm $ServiceName)) {
                throw "[$( $MyInvocation.MyCommand )] The service '$ServiceName' is not running. Installation not successful."
        }
}
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 )"
            throw $_
        }
    }
}
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 )"
            throw $_
        }
    }
}
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.PSObject.Properties[$Region].Value
                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 )"
            throw $_
        }
    }
}
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 )"
            throw $_
        }
    }
}
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 )"
            throw $_
        }
    }
    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 )"
        throw $_
    }
}
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) {
        throw "Unable to invoke SSM document '$Name' on instance '$InstanceId' in region '$Region'."
    }
    return $command
}
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 )"
            throw $_
        }
    }
}
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 42) {
                    Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] The AWS EC2 instance '$InstanceName' failed to reach 'running' state or to pass the reachability tests within a 5 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 )"
            throw
        }
    }
}
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 )"
            throw
        }
    }
}
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 )"
            throw
        }
    }
}
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 )"
            throw
        }
    }
}
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 )"
            throw
        }
    }
}
function New-KeyValueStore {
    param(
        [string]
        $Path
    )
    if (!$Path) {
        $Path = Get-LocalStateFilePath
    }
    return [KeyValueStore]::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 )"
            throw
        }
    }
}
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-ItemWithRetry {
    param (
        [Parameter(Mandatory)]
        [string]$Path,
        [int]$WaitTimeInSeconds = 3,
        [int]$MaxRetryCount = 60,
        [switch]$Recurse
    )
    $retryCount = 0
    $success = $false
    do {
        $retryCount++
        try {
            Remove-Item -Path $Path -Force -Recurse:$Recurse -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) {
        throw "Failed to remove $Path after $retryCount attempts."
    }
}
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 )"
            throw
        }
    }
}
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 )"
            throw
        }
    }
}
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..."
                    throw
                }
                $sensorDefaults = (Get-Defaults).PRTG.Sensors.PSObject.Properties | foreach -begin { $ht = @{ } } -process { $ht[$_.Name] = $_.Value } -end { $ht }
                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 )"
            throw
        }
    }
}
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 )"
            throw $_
        }
    }
}
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 )"
            throw $_
        }
    }
}
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) {
            throw "A service with the name $Name is not found."
        }
        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
    }
    throw "The service $Name failed to stop within the specified retry timeout of $($Retries * $WaitSeconds) seconds."
}
function Test-IsAppInstalled {
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $SearchTerm
    )
    if (-not $IsWindows) {
        throw "[$( $MyInvocation.MyCommand )] This cmdlet is only supported on Windows."
    }
    $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
    }
    $webappDir = Get-TomcatWebAppPath
    $browserPropertiesFile = Get-BrowserPlatformPropertiesFilePath -TomcatWebAppPath $webappDir
    if (Test-Path -Path $browserPropertiesFile) {
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Is a Browser System."
        return $true
    }
    else {
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Is NOT a Browser System."
        return $false
    }
}
function Test-IsServiceRunning {
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $SearchTerm
    )
    if (-not $IsWindows) {
        throw "[$( $MyInvocation.MyCommand )] This cmdlet is only supported on Windows."
    }
    $service = Get-Service -Name $SearchTerm -ErrorAction SilentlyContinue
    if (-not$service) {
        throw "[$( $MyInvocation.MyCommand )] No service with the name '$SearchTerm' was found."
    }
    if ($service.Count -gt 1) {
        throw "[$( $MyInvocation.MyCommand )] More than one service with the name '$SearchTerm' was found. Please specify a more specific search term."
    }
    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
    )
    try {
        $xmlDocument = New-Object System.Xml.XmlDocument
        $xmlDocument.LoadXml($XmlString)
        return $true
    } catch {
        return $false
    }
}
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)) {
                throw "[$( $MyInvocation.MyCommand )] mol2name_batch.exe is not able to genereate a sample output using the sample data file."
            }
        }
        if ($IsLinux) {
            & (Join-Path $pwd "mol2name_batch") $sampleDataFile $outputFile
            if (!(Test-Path $outputFile)) {
                throw "[$( $MyInvocation.MyCommand )] mol2name_batch is not able to genereate a sample output using the sample data file."
            }
        }
        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.PSObject.Properties) {
            $expectedFileName = $expectedFile.Name
            $expectedFileHash = $expectedFile.Value.ToLower()
            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) {
            throw "The command ID is missing."
        }
        $invokeResult = Get-SSMCommandInvocationDetail -InstanceId $InstanceId -CommandId $Result.CommandId -Region $Region
        if ($invokeResult.Status -ne "Success") { throw "The operation failed." }
        $stdOutResult = $invokeResult.StandardOutputContent | ConvertFrom-Json
        if ($stdOutResult.Status -ne "OK") {
            throw $stdOutResult.Message
        }
        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
            throw $message
        }
        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
                throw "[$($MyInvocation.MyCommand)] Failed SSM command '$CommandId' with doc '$($commandResult.DocumentName)' in region '$Region'."
            }
            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) {
            throw "Instance '$InstanceId' is not registered to SSM."
        }
        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) {
                throw "Instance '$InstanceId' is not reachable via SSM."
            }
            return $false
        }
    }
    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Instance '$InstanceId' is reachable via SSM."
    return $true
}
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 {
                    throw $_
                }
            }
        }
        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) {
                throw "Max attempts reached. SSM command '$CommandId' did not complete within the time limit."
            }
        }
        catch {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] Exception caught: Unable to retrieve the status of SSM command '$CommandId'."
            throw $_
        }
    }
}
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-Log {
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateSet("Info", "Warning", "Error", "Critical", IgnoreCase = $false)]
        [string]$LogLevel,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [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
    }
}
function Write-LogCritical {
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]$Message,
        [Parameter(Mandatory = $false)]
        [switch]$ThrowException
    )
    Write-Log -LogLevel "Critical" -Message $Message
    if ($ThrowException) {
        throw $Message
    }
}
function Write-LogError {
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]$Message
    )
    Write-Log -LogLevel "Error" -Message $Message
}
function Write-LogInfo {
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]$Message
    )
    Write-Log -LogLevel "Info" -Message $Message
}
function Write-LogSeparator {
    Write-Log -LogLevel "Info" -Message "---------------------------------------------------------------------"
}
function Write-LogWarning {
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]$Message
    )
    Write-Log -LogLevel "Warning" -Message $Message
}
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) {
                throw "Failed to write to file '$FilePath' after $($RetryCount + 1) attempts."
            }
            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 is NOT in allowed values: key: $Key Value: $myValue" 
    throw "[$( $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-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 = @{
                            Version               = "UNKNOWN"
                            Mol2ImgChecksum       = "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.Version = $openEyeMeta.Mol2ImgVersion ?? $returnObj.AppInfo.BrowserPlatform.Plugins.OpenEye.Version
                $returnObj.AppInfo.BrowserPlatform.Plugins.OpenEye.Mol2ImgChecksum = $openEyeMeta.Mol2ImgChecksum ?? $returnObj.AppInfo.BrowserPlatform.Plugins.OpenEye.Mol2ImgChecksum
                $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-LocalDiscoveryFilePath
        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-LogWarning "[$( $MyInvocation.MyCommand )] No patching completed yet, not expected"
        throw "Unable to find PatchInventoryFromLastOperation.json file, has something changed?"
      }
      $myInventory = Get-PatchInventoryOnWindows -FilePath $windowsFile
    }
    else {
      if (-not (Test-Path -Path $linuxFile)) {
        Write-LogWarning "[$( $MyInvocation.MyCommand )] No patching completed yet, not expected"
        throw "Unable to find PatchInventoryFromLastOperation.json file, has something changed?"
      }
      $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)) {
                    throw "[$( $MyInvocation.MyCommand )] The CrowdStrike Agent is not supported on this version of Windows. Supported Windows versions are $($SupportedWindowsVersions -join ', ')."
                }
            }
            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)"
            throw $_
        }
    }
    else {
        Write-LogInfo "[$( $MyInvocation.MyCommand )] Installing the CrowdStrike Agent using this module is only supported on Windows for now."
    }
}
function Install-DTXCrowdStrikeAgentViaS3 {
    [CmdletBinding()]
    param(
        [string]
        $CrowdStrikeCustomerIdSSMParameterName,
        [string]
        $BucketName,
        [string]
        $BucketObjectKey,
        [ValidateNotNullOrEmpty()]
        $SupportedWindowsVersions = @("2012", "2016", "2019", "2022"),
        [switch]
        $Force
    )
    begin {
        $defaults = Get-Defaults
        if ($CrowdStrikeCustomerIdSSMParameterName -eq $null) {
            $CrowdStrikeCustomerIdSSMParameterName = $defaults.AWS.SSM.ParameterStore.Parameters.CrowdStrikeCustomerId
        }
        if ($BucketName -eq $null) {
            $BucketName = $defaults.AWS.S3.Buckets.SoftwareRepository.Name
        }
        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
        }
    }
    process {
        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] This function is deprecated and will be removed in a future release. Please use the Install-DTXCrowdStrikeAgent function instead."
        if ($IsWindows) {
            try {
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Checking if the CrowdStrike Agent is installed and running..."
                if ($BucketObjectKey -eq $null) {
                    $BucketObjectKey = $defaults.AWS.S3.Buckets.SoftwareRepository.Objects.CrowdStrikeAgentInstaller.Windows
                }
                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."
                    exit 0
                }
                Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] The CrowdStrike Agent is not installed or running. Starting installation..."
                if (-not (_Test-IsWindowsVersionSupported)) {
                    throw "[$( $MyInvocation.MyCommand )] The CrowdStrike Agent is not supported on this version of Windows. Supported Windows versions are $($SupportedWindowsVersions -join ', ')."
                }
                $customerId = (Get-SSMParameterValue -Name $CrowdStrikeCustomerIdSSMParameterName -Region $SSMParameterStoreRegion -WithDecryption:$true -ErrorAction Stop).Parameters[0].Value
                $installationFile = Get-FileFromS3 -BucketName $BucketName -BucketKey $BucketObjectKey -PreserveFileName
                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-LogError -Message "[$( $MyInvocation.MyCommand )] Error message: $($_.Exception.Message)"
                exit 1
            }
        }
        else {
            Write-LogInfo "[$( $MyInvocation.MyCommand )] Installing the CrowdStrike Agent using this module is only supported on Windows for now."
            Write-LogInfo "[$( $MyInvocation.MyCommand )] Exiting..."
            exit 0
        }
    }  
}
function Install-DTXOpenEye {
    param(
        [Parameter(Mandatory)]
        [ValidateSet(
            "2024"
        )]
        [string]$Version,
        [switch]$Backup
    )
    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..."
            if (!(Get-DTXSystem).ComputedQueries.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 (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 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 -ThrowException "[$( $MyInvocation.MyCommand )] This script must be run on an EC2 instance that has access to the EC2 instance metadata service. Error: $_"
    }
    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 -ThrowException "[$( $MyInvocation.MyCommand )] Failed to assume IAM role: $IAMRoleArn. Error: $_"
    }
    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. Error: $_"
            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-DTXCrowdStrikeAgent -WhatIf:$DryRun
        Write-LogSeparator
        Write-LogSeparator
        Uninstall-DTXAutomoxAgent -WhatIf:$DryRun
        Write-LogSeparator
    }
    catch {
        Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] An error occurred. Error: $_"
        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
        }
        catch {
            Write-LogCritical -Message "[$( $MyInvocation.MyCommand )] An error occurred. Error: $_"
            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. Error: $_"
            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_mon",
            "pre_prod_tues",
            "pre_prod_wed",
            "pre_prod_thu",
            "pre_prod_fri",
            "pre_prod_sat",
            "prod_sun",
            "prod_mon",
            "prod_tues",
            "prod_wed",
            "prod_thu",
            "prod_fri",
            "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
    )
    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 = [KeyValueStore]::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 "##################################################################################################"
}
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 -ThrowException "[$( $MyInvocation.MyCommand )] Failed to generate the DTX discovery JSON file. Error: $_"
            }
        }
    }
}
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 )] Error message: $($_.Exception.Message)"
            }
        }
        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 )"
        throw
    }
    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)."
                throw
            }
            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."
                throw "The system needs to be investigated manually."
            }
            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
        }
    }
    End {
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] ################################################################################"
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] END: DOTMATICS - Tomcat Key Store Updater"
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] ################################################################################"
    }
}
function Set-DTXTomcatServerSettings {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateSet(1)]
        [int]$SettingsVersion
    )
    $sysInfo = Get-DTXSystem
    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Checking if Tomcat is running on this system..."
    if ((-not $sysInfo.ComputedQueries.IsWebServer.Status) -or (-not $sysInfo.ComputedQueries.IsWebServer.AppName -eq "Tomcat")) {
        Write-LogCritical -ThrowException -Message "[$( $MyInvocation.MyCommand )] Tomcat server not detected on this host."
    }
    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Tomcat is running on this system"
    $serverSettings = (Get-Defaults).Tomcat.ServerSettings | Where-Object { $_.Version -eq $SettingsVersion }
    if (-not $serverSettings) {
        Write-LogCritical -ThrowException -Message "[$( $MyInvocation.MyCommand )] Your selected ServerSettings version $SettingsVersion is not valid. Please try again or report this issue to the script maintainers."
    }
    $tomcatVersion = $sysInfo.AppInfo.Tomcat.Version
    $javaVersion = $sysInfo.AppInfo.Tomcat.Java.Version
    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Detected Apache Tomcat version: $tomcatVersion"
    Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Detected Java version: $javaVersion"
    $selectedSettings = $serverSettings |
    Where-Object { [System.Version]$tomcatVersion -ge [System.Version] $_.TomcatMinVersion } |
    Where-Object { [System.Version]$javaVersion -ge [System.Version] $_.JavaMinVersion } |
    Sort-Object { [System.Version] $_.TomcatMinVersion }, { [System.Version] $_.JavaMinVersion } -Descending
    if (-not $selectedSettings -or $selectedSettings.Count -eq 0) {
        $supportedTomcatVersions = $serverSettings | Select-Object -ExpandProperty TomcatMinVersion -Unique
        $supportedJavaVersions = $serverSettings | Select-Object -ExpandProperty JavaMinVersion -Unique
        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Minimum supported Tomcat versions: $($supportedTomcatVersions -join ", ")"
        Write-LogWarning -Message "[$( $MyInvocation.MyCommand )] Minimum supported Java versions: $($supportedJavaVersions -join ", ")"
        Write-LogCritical -ThrowException -Message "[$( $MyInvocation.MyCommand )] No server settings found for Tomcat ($tomcatVersion) or Java ($javaVersion)."
    }
    $selectedSettings = $selectedSettings[0]
    $serverXmlPath = $sysInfo.AppInfo.Tomcat.ServerXML.Path
    try {
        [xml]$serverXmlContents = Get-Content -Path $serverXmlPath -ErrorAction Stop
    }
    catch {
        Write-LogCritical -ThrowException -Message "[$( $MyInvocation.MyCommand )] Unable to read the Tomcat server XML file. Please check the file path and permissions."
    }
    $connectorsUpdated = $false
    foreach ($connector in $serverXmlContents.Server.Service.Connector) {
        if ((($connector.protocol -like "*HTTP/1.1*") -or ($connector.protocol -like "*Http11NioProtocol*")) -and $connector.SSLEnabled -eq "true") {
            $connectorsUpdated = $true
            Update-XmlAttribute -Element $connector -Attribute "enableLookups" -Value $selectedSettings.EnableLookups
            Update-XmlAttribute -Element $connector -Attribute "sslEnabledProtocols" -Value ($selectedSettings.SSLSettings.Protocols -join ",")
            Update-XmlAttribute -Element $connector -Attribute "ciphers" -Value $selectedSettings.SSLSettings.ciphers
            $http2 = $connector.SelectSingleNode("UpgradeProtocol[@className='org.apache.coyote.http2.Http2Protocol']")
            if ($selectedSettings.EnableHTTP2) {
                if (-not $http2) {
                    $newHttp2 = $serverXmlContents.CreateElement("UpgradeProtocol")
                    $newHttp2.SetAttribute("className", "org.apache.coyote.http2.Http2Protocol")
                    $connector.AppendChild($newHttp2) | Out-Null
                }
            }
            else {
                if ($http2) {
                    $connector.RemoveChild($http2) | Out-Null
                }
            }
        }
    }
    if (-not $connectorsUpdated) {
        Write-LogCritical -ThrowException -Message "[$( $MyInvocation.MyCommand )] Unable to find any HTTP connectors with SSL enabled. Please make sure the Tomcat server XML file is configured correctly."
    }
    $serviceXmlNode = $serverXmlContents.Server.Service
    $http2OnServiceXmlNode = $serviceXmlNode.SelectSingleNode("UpgradeProtocol[@className='org.apache.coyote.http2.Http2Protocol']")
    if ($http2OnServiceXmlNode -and ($http2OnServiceXmlNode.ParentNode -eq $serviceXmlNode)) {
        $serviceXmlNode.RemoveChild($http2OnServiceXmlNode) | Out-Null
    }
    $formattedXml = $serverXmlContents | ConvertTo-FormattedXmlString
    if (-not (Test-IsValidXml -XmlString $formattedXml)) {
        Write-LogCritical -ThrowException -Message "[$( $MyInvocation.MyCommand )] Unable to generate a valid XML file to save. Please report this error to the script maintainer."
    }
    if ($PSCmdlet.ShouldProcess($serverXmlPath, "Save changes to XML file")) {
        Write-StringToFileWithRetry -StringContent $formattedXml -FilePath $serverXmlPath -RetryCount 3 -WaitSeconds 5
        Write-LogInfo -Message "[$( $MyInvocation.MyCommand )] Successfully updated the Tomcat server settings. You need to restart the Tomcat service for the changes to take effect."
    }
    else {
        Write-Host "################################################################################"
        Write-Host $formattedXml
        Write-Host "################################################################################"
    }
}
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) {
            throw "Failed to uninstall Automox Agent"
        }
    }
}
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-LocalDiscoveryFilePath) -Force
        }
    }
}