Public/psf-real-time-response.ps1

function Get-FalconQueue {
    [CmdletBinding()]
    param(
        [Parameter(Position = 1)]
        [int] $Days,

        [Parameter(Position = 2)]
        [ValidateSet('agent_version', 'cid', 'external_ip', 'first_seen', 'host_hidden_status', 'hostname',
            'last_seen', 'local_ip', 'mac_address', 'os_build', 'os_version', 'platform_name', 'product_type',
            'product_type_desc', 'reduced_functionality_mode', 'serial_number', 'system_manufacturer',
            'system_product_name', 'tags')]
        [array] $Include
    )
    begin {
        $Days = if ($PSBoundParameters.Days) { $PSBoundParameters.Days } else { 7 }
        # Properties to capture from request results
        $Properties = @{
            Session = @('aid', 'user_id', 'user_uuid', 'id', 'created_at', 'deleted_at', 'status')
            Command = @('stdout', 'stderr', 'complete')
        }
        # Define output path
        $OutputFile = Join-Path -Path (Get-Location).Path -ChildPath "FalconQueue_$(
            Get-Date -Format FileDateTime).csv"

    }
    process {
        try {
            $SessionParam = @{
                Filter   = "(deleted_at:null+commands_queued:1),(created_at:>'last $Days days'+commands_queued:1)"
                Detailed = $true
                All      = $true
                Verbose  = $true
            }
            $Sessions = Get-FalconSession @SessionParam | Select-Object id, device_id
            if (-not $Sessions) {
                throw "No queued Real-time Response sessions available."
            }
            [array] $HostInfo = if ($PSBoundParameters.Include) {
                # Capture host information for eventual output
                Get-FalconHost -Ids ($Sessions.device_id | Group-Object).Name | Select-Object @(
                    $PSBoundParameters.Include + 'device_id')
            }
            foreach ($Session in (Get-FalconSession -Ids $Sessions.id -Queue -Verbose)) {
                @($Session.Commands).foreach{
                    # Create output for each individual command in queued session
                    $Item = [PSCustomObject] @{}
                    @($Session | Select-Object $Properties.Session).foreach{
                        @($_.PSObject.Properties).foreach{
                            # Add session properties with 'session' prefix
                            $Name = if ($_.Name -match '^(id|(created|deleted|updated)_at|status)$') {
                                "session_$($_.Name)"
                            } else {
                                $_.Name
                            }
                            Add-Property -Object $Item -Name $Name -Value $_.Value
                        }
                    }
                    @($_.PSObject.Properties).foreach{
                        # Add command properties
                        $Name = if ($_.Name -match '^((created|deleted|updated)_at|status)$') {
                            "command_$($_.Name)"
                        } else {
                            $_.Name
                        }
                        Add-Property -Object $Item -Name $Name -Value $_.Value
                    }
                    if ($Item.command_status -eq 'FINISHED') {
                        # Update command properties with results
                        $Param = @{
                            CloudRequestId = $Item.cloud_request_id
                            Verbose        = $true
                            ErrorAction    = 'SilentlyContinue'
                        }
                        $ConfirmCmd = Get-RtrCommand $Item.base_command -ConfirmCommand
                        @(& $ConfirmCmd @Param | Select-Object $Properties.Command).foreach{
                            @($_.PSObject.Properties).foreach{
                                Add-Property -Object $Item -Name "command_$($_.Name)" -Value $_.Value
                            }
                        }
                    } else {
                        @('command_complete', 'command_stdout', 'command_stderr').foreach{
                            # Add empty command output
                            $Value = if ($_ -eq 'command_complete') { $false } else { $null }
                            Add-Property -Object $Item -Name $_ -Value $Value
                        }
                    }
                    if ($PSBoundParameters.Include -and $HostInfo) {
                        @($HostInfo.Where({ $_.device_id -eq $Item.aid })).foreach{
                            @($_.PSObject.Properties.Where({ $_.Name -ne 'device_id' })).foreach{
                                # Add 'Include' properties
                                Add-Property -Object $Item -Name $_.Name -Value $_.Value
                            }
                        }
                    }
                    # Export using 'Export-FalconReport' and suppress 'Get-ChildItem' output
                    [void] ($Item | Export-FalconReport $OutputFile)
                }
            }
        } catch {
            throw $_
        } finally {
            if (Test-Path $OutputFile) {
                Get-ChildItem $OutputFile | Select-Object FullName, Length, LastWriteTime
            }
        }
    }
}
function Invoke-FalconDeploy {
    [CmdletBinding()]
    [CmdletBinding(DefaultParameterSetName = 'HostIds')]
    param(
        [Parameter(ParameterSetName = 'HostIds', Mandatory = $true, Position = 1)]
        [Parameter(ParameterSetName = 'GroupId', Mandatory = $true, Position = 1)]
        [ValidateScript({
            if (Test-Path -Path $_ -PathType Leaf) {
                $true
            } else {
                throw "Cannot find path '$_' because it does not exist or is a directory."
            }
        })]
        [string] $Path,

        [Parameter(ParameterSetName = 'HostIds', Position = 2)]
        [Parameter(ParameterSetName = 'GroupId', Position = 2)]
        [string] $Arguments,

        [Parameter(ParameterSetName = 'HostIds', Position = 3)]
        [Parameter(ParameterSetName = 'GroupId', Position = 3)]
        [ValidateRange(30,600)]
        [int] $Timeout,

        [Parameter(ParameterSetName = 'HostIds')]
        [Parameter(ParameterSetName = 'GroupId')]
        [boolean] $QueueOffline,

        [Parameter(ParameterSetName = 'HostIds', Mandatory = $true)]
        [ValidatePattern('^\w{32}$')]
        [array] $HostIds,

        [Parameter(ParameterSetName = 'GroupId', Mandatory = $true)]
        [ValidatePattern('^\w{32}$')]
        [string] $GroupId
    )
    begin {
        # Fields to collect from 'Put' files list
        $PutFields = @('id', 'name', 'created_timestamp', 'modified_timestamp', 'sha256')
        function Write-RtrResult ($Object, $Step, $BatchId) {
            # Create output, append results and output to CSV
            $Output = foreach ($Item in $Object) {
                [PSCustomObject] @{
                    aid              = $Item.aid
                    batch_id         = $BatchId
                    session_id       = $null
                    cloud_request_id = $null
                    deployment_step  = $Step
                    complete         = $false
                    offline_queued   = $false
                    errors           = $null
                    stderr           = $null
                    stdout           = $null
                }
            }
            Get-RtrResult -Object $Object -Output $Output | Export-Csv $OutputFile -Append -NoTypeInformation
        }
        # Set output file and executable details
        $OutputFile = Join-Path -Path (Get-Location).Path -ChildPath "FalconDeploy_$(
            Get-Date -Format FileDateTime).csv"

        $FilePath = $Script:Falcon.Api.Path($PSBoundParameters.Path)
        $Filename = "$([System.IO.Path]::GetFileName($FilePath))"
        $ProcessName = "$([System.IO.Path]::GetFileNameWithoutExtension($FilePath))"
        [array] $HostArray = if ($PSBoundParameters.GroupId) {
            if ((Get-FalconHostGroupMember -Id $PSBoundParameters.GroupId -Total) -gt 10000) {
                # Stop if number of members exceeds API limit
                throw "Group size exceeds maximum number of results [10,000]"
            } else {
                # Find Host Group member identifiers
                ,(Get-FalconHostGroupMember -Id $PSBoundParameters.GroupId -Detailed -All |
                    Select-Object device_id, platform_name)
            }
        } else {
            # Use provided Host identifiers
            ,(Get-FalconHost -Ids $PSBoundParameters.HostIds | Select-Object device_id, platform_name)
        }
        if ($HostArray) {
            try {
                Write-Host "Checking cloud for existing file..."
                $CloudFile = @(Get-FalconPutFile -Filter "name:['$Filename']" -Detailed |
                Select-Object $PutFields).foreach{
                    [PSCustomObject] @{
                        id                 = $_.id
                        name               = $_.name
                        created_timestamp  = [datetime] $_.created_timestamp
                        modified_timestamp = [datetime] $_.modified_timestamp
                        sha256             = $_.sha256
                    }
                }
                $LocalFile = @(Get-ChildItem $FilePath | Select-Object CreationTime, Name, LastWriteTime).foreach{
                    [PSCustomObject] @{
                        name               = $_.Name
                        created_timestamp  = $_.CreationTime
                        modified_timestamp = $_.LastWriteTime
                        sha256             = ((Get-FileHash -Algorithm SHA256 -Path $FilePath).Hash).ToLower()
                    }
                }
                if ($LocalFile -and $CloudFile) {
                    if ($LocalFile.sha256 -eq $CloudFile.sha256) {
                        Write-Host "Matched hash values between local and cloud files..."
                    } else {
                        Write-Host "[CloudFile]"
                        $CloudFile | Select-Object name, created_timestamp, modified_timestamp, sha256 |
                            Format-List | Out-Host
                        Write-Host "[LocalFile]"
                        $LocalFile | Select-Object name, created_timestamp, modified_timestamp, sha256 |
                            Format-List | Out-Host
                        $FileChoice = $host.UI.PromptForChoice(
                            "'$Filename' exists in your 'Put' Files. Use existing version?", $null,
                            [System.Management.Automation.Host.ChoiceDescription[]] @("&Yes", "&No"), 0)
                        if ($FileChoice -eq 0) {
                            Write-Host "Proceeding with CloudFile: $($CloudFile.id)..."
                        } else {
                            $RemovePut = Remove-FalconPutFile -Id $CloudFile.id
                            if ($RemovePut.writes.resources_affected -eq 1) {
                                Write-Host "Removed CloudFile: $($CloudFile.id)"
                            }
                        }
                    }
                }
            } catch {
                throw $_
            }
        }
    }
    process {
        if ($HostArray) {
            $AddPut = if ($RemovePut.writes.resources_affected -eq 1 -or !$CloudFile) {
                Write-Host "Uploading $Filename..."
                $Param = @{
                    Path        = $FilePath
                    Name        = $Filename
                    Description = "$ProcessName"
                    Comment     = 'PSFalcon: Invoke-FalconDeploy'
                }
                Send-FalconPutFile @Param
            }
            if ($AddPut.writes.resources_affected -ne 1 -and !$CloudFile.id) {
                throw "Upload failed."
            }
            try {
                for ($i = 0; $i -lt ($HostArray | Measure-Object).Count; $i += 1000) {
                    $Param = @{ HostIds = ($HostArray[$i..($i + 999)]).device_id }
                    switch -Regex ($PSBoundParameters.Keys) {
                        '(QueueOffline|Timeout)' { $Param[$_] = $PSBoundParameters.$_ }
                    }
                    $Session = Start-FalconSession @Param
                    $SessionHosts = if ($Session.batch_id) {
                        # Output result to CSV and return list of successful 'session_start' hosts
                        Write-RtrResult -Object $Session.hosts -Step 'session_start' -BatchId $Session.batch_id
                        ($Session.hosts | Where-Object { $_.complete -eq $true -or
                            $_.offline_queued -eq $true }).aid
                    }
                    if ($SessionHosts) {
                        # Change to a 'temp' directory for each device, by platform
                        Write-Host "Initiated session with $(($SessionHosts | Measure-Object).Count) host(s)..."
                        foreach ($Pair in (@{
                            Windows = ($HostArray | Where-Object { $SessionHosts -contains $_.device_id -and
                                $_.platform_name -eq 'Windows' }).device_id
                            Mac     = ($HostArray | Where-Object { $SessionHosts -contains $_.device_id -and
                                $_.platform_name -eq 'Mac' }).device_id
                            Linux   = ($HostArray | Where-Object { $SessionHosts -contains $_.device_id -and
                                $_.platform_name -eq 'Linux' }).device_id
                        }).GetEnumerator().Where({ $_.Value })) {
                            $Param = @{
                                BatchId         = $Session.batch_id
                                Command         = 'cd'
                                OptionalHostIds = $Pair.Value
                            }
                            if ($PSBoundParameters.Timeout) {
                                $Param['Timeout'] = $PSBoundParameters.Timeout
                            }
                            $TempDir = switch ($Pair.Key) {
                                'Windows' { '\Windows\Temp' }
                                'Mac'     { '/tmp' }
                                'Linux'   { '/tmp' }
                            }
                            $Param['Arguments'] = $TempDir
                            $CmdCd = Invoke-FalconAdminCommand @Param
                            $CdHosts = if ($CmdCd) {
                                # Output result to CSV and return list of successful 'cd_temp' hosts
                                Write-RtrResult -Object $CmdCd -Step 'cd_temp' -BatchId $Session.batch_id
                                ,($CmdCd | Where-Object { ($_.complete -eq $true -and !$_.stderr) -or
                                    $_.offline_queued -eq $true }).aid
                            }
                            $PutHosts = if ($CdHosts) {
                                # Invoke 'put' on successful hosts
                                Write-Host ("Sending $Filename to $(($CdHosts | Measure-Object).Count)" +
                                    " $($Pair.Key) host(s)...")
                                $Param = @{
                                    BatchId         = $Session.batch_id
                                    Command         = 'put'
                                    Arguments       = $Filename
                                    OptionalHostIds = $CdHosts
                                }
                                if ($PSBoundParameters.Timeout) {
                                    $Param['Timeout'] = $PSBoundParameters.Timeout
                                }
                                $CmdPut = Invoke-FalconAdminCommand @Param
                                if ($CmdPut) {
                                    # Output result to CSV and return list of successful 'put_file' hosts
                                    Write-RtrResult -Object $CmdPut -Step 'put_file' -BatchId $Session.batch_id
                                    ($CmdPut | Where-Object { ($_.complete -eq $true -and !$_.stderr) -or
                                        $_.offline_queued -eq $true }).aid
                                }
                            }
                            if ($PutHosts -and $Pair.Key -eq 'Linux') {
                                # Modify 'put' file if running on Linux
                                Write-Host ("Modifying $Filename on $(($PutHosts | Measure-Object).Count)" +
                                    " $($Pair.Key) host(s)...")
                                $ModParam = @{
                                    BatchId         = $Session.batch_id
                                    Command         = 'runscript'
                                    Arguments       = '-Raw=```chmod +x ' + "$($TempDir)/$($Filename)" + '```'
                                    OptionalHostIds = $PutHosts
                                }
                                $CmdMod = Invoke-FalconAdminCommand @ModParam
                                $PutHosts = if ($CmdMod) {
                                    # Output result to CSV and return list of successful 'mod_file' hosts
                                    Write-RtrResult -Object $CmdMod -Step 'mod_file' -BatchId $Session.batch_id
                                    ($CmdMod | Where-Object { ($_.complete -eq $true -and !$_.stderr) -or
                                        $_.offline_queued -eq $true }).aid
                                }
                            }
                            if ($PutHosts) {
                                # Invoke 'run'
                                Write-Host ("Starting $Filename on $(($PutHosts | Measure-Object).Count)" +
                                    " $($Pair.Key) host(s)...")
                                $Arguments = if ($Pair.Key -eq 'Windows') {
                                    "$($TempDir)\$($Filename)"
                                } else {
                                    "$($TempDir)/$($Filename)"
                                }
                                if ($PSBoundParameters.Arguments) {
                                    $Arguments += " -CommandLine=`"$($PSBoundParameters.Arguments)`""
                                }
                                $Param = @{
                                    BatchId         = $Session.batch_id
                                    Command         = 'run'
                                    Arguments       = $Arguments
                                    OptionalHostIds = $PutHosts
                                }
                                if ($PSBoundParameters.Timeout) {
                                    $Param['Timeout'] = $PSBoundParameters.Timeout
                                }
                                $CmdRun = Invoke-FalconAdminCommand @Param
                                if ($CmdRun) {
                                    # Output result to CSV
                                    Write-RtrResult -Object $CmdRun -Step 'run_file' -BatchId $Session.batch_id
                                }
                            }
                        }
                    }
                }
            } catch {
                throw $_
            } finally {
                if (Test-Path $OutputFile) {
                    Get-ChildItem $OutputFile | Select-Object FullName, Length, LastWriteTime
                }
            }
        }
    }
}
function Invoke-FalconRtr {
    [CmdletBinding(DefaultParameterSetName = 'HostId')]
    param(
        [Parameter(ParameterSetName = 'HostId', Mandatory = $true, Position = 1)]
        [Parameter(ParameterSetName = 'HostIds', Mandatory = $true, Position = 1)]
        [Parameter(ParameterSetName = 'GroupId', Mandatory = $true, Position = 1)]
        [ValidateSet('cat', 'cd', 'clear', 'cp', 'csrutil', 'cswindiag', 'encrypt', 'env', 'eventlog', 'filehash',
            'get', 'getsid', 'history', 'ifconfig', 'ipconfig', 'kill', 'ls', 'map', 'memdump', 'mkdir', 'mount',
            'mv', 'netstat', 'ps', 'put', 'put-and-run', 'reg delete', 'reg load', 'reg query', 'reg set',
            'reg unload', 'restart', 'rm', 'run', 'runscript', 'shutdown', 'umount', 'unmap', 'update history',
            'update install', 'update list', 'users', 'xmemdump', 'zip')]
        [string] $Command,

        [Parameter(ParameterSetName = 'HostId', Position = 2)]
        [Parameter(ParameterSetName = 'HostIds', Position = 2)]
        [Parameter(ParameterSetName = 'GroupId', Position = 2)]
        [string] $Arguments,

        [Parameter(ParameterSetName = 'HostIds', Position = 3)]
        [Parameter(ParameterSetName = 'GroupId', Position = 3)]
        [ValidateRange(30,600)]
        [int] $Timeout,

        [Parameter(ParameterSetName = 'HostId')]
        [Parameter(ParameterSetName = 'HostIds')]
        [Parameter(ParameterSetName = 'GroupId')]
        [boolean] $QueueOffline,

        [Parameter(ParameterSetName = 'HostId', ValueFromPipeline = $true, Mandatory = $true)]
        [ValidatePattern('^\w{32}$')]
        [Alias('device_id')]
        [string] $HostId,

        [Parameter(ParameterSetName = 'HostIds', Mandatory = $true)]
        [ValidatePattern('^\w{32}$')]
        [array] $HostIds,

        [Parameter(ParameterSetName = 'GroupId', Mandatory = $true)]
        [ValidatePattern('^\w{32}$')]
        [string] $GroupId
    )
    begin {
        if ($PSCmdlet.ParameterSetName -ne 'HostId') {
            function Initialize-Output ([array] $HostIds) {
                # Create initial array of output for each host
                ($HostIds).foreach{
                    $Item = [PSCustomObject] @{
                        aid              = $_
                        batch_id         = $null
                        session_id       = $null
                        cloud_request_id = $null
                        complete         = $false
                        offline_queued   = $false
                        errors           = $null
                        stderr           = $null
                        stdout           = $null
                    }
                    if ($InvokeCmd -eq 'Invoke-FalconBatchGet') {
                        Add-Property -Object $Item -Name 'batch_get_cmd_req_id' -Value $null
                    }
                    if ($PSBoundParameters.GroupId) {
                        Add-Property -Object $Item -Name 'host_group_id' -Value $PSBoundParameters.GroupId
                    }
                    $Item
                }
            }
            if ($PSBoundParameters.Timeout -and $PSBoundParameters.Command -eq 'runscript' -and
            $PSBoundParameters.Arguments -notmatch '-Timeout=\d{2,3}') {
                # Force 'Timeout' into 'Arguments' when using 'runscript'
                $PSBoundParameters.Arguments += " -Timeout=$($PSBoundParameters.Timeout)"
            }
            # Determine Real-time Response command to invoke
            $InvokeCmd = if ($PSBoundParameters.Command -eq 'get') {
                'Invoke-FalconBatchGet'
            } else {
                Get-RtrCommand $PSBoundParameters.Command
            }
        }
    }
    process {
        try {
            if ($PSCmdlet.ParameterSetName -eq 'HostId') {
                $InitParam = @{
                    HostId       = $PSBoundParameters.HostId
                    QueueOffline = if ($PSBoundParameters.QueueOffline -eq $true) { $true } else { $false }
                }
                $Init = Start-FalconSession @InitParam
                $Request = if ($Init.session_id) {
                    # Set baseline command parameters
                    $CmdParam = @{ SessionId = $Init.session_id }
                    switch -Regex ($PSBoundParameters.Keys) {
                        '^(Command|Arguments)$' { $CmdParam[$_] = $PSBoundParameters.$_ }
                    }
                    Invoke-FalconAdminCommand @CmdParam
                }
                if ($Init.offline_queued -eq $false -and $Request.cloud_request_id) {
                    do {
                        # Retry command confirmation until result is provided
                        Start-Sleep -Seconds 5
                        $Confirm = Confirm-FalconAdminCommand -CloudRequestId $Request.cloud_request_id
                    } until (
                        $Confirm.complete -ne $false -or $Confirm.stdout -or $Confirm.stderr
                    )
                    $Confirm | ForEach-Object {
                        $CmdId = if ($_.task_id -and !$_.cloud_request_id) {
                            # Rename 'task_id' to 'cloud_request_id'
                            Add-Property -Object $_ -Name 'cloud_request_id' -Value $_.task_id
                            $_.PSObject.Properties.Remove('task_id')
                            'cloud_request_id'
                        } else {
                            'task_id'
                        }
                        $_ | Select-Object session_id, $CmdId, complete, stdout, stderr | ForEach-Object {
                            if ($_.stdout -and $PSBoundParameters.Command -eq 'runscript') {
                                # Attempt to convert 'stdout' from Json for 'runscript'
                                $StdOut = try { $_.stdout | ConvertFrom-Json } catch { $null }
                                if ($StdOut) { $_.stdout = $StdOut }
                            }
                            $_
                        }
                    }
                } else {
                    $Request
                }
            } else {
                [array] $HostArray = if ($PSCmdlet.ParameterSetName -eq 'GroupId') {
                    if ((Get-FalconHostGroupMember -Id $PSBoundParameters.GroupId -Total) -gt 10000) {
                        # Stop if number of members exceeds API limit
                        throw "Group size exceeds maximum number of results [10,000]"
                    } else {
                        # Find Host Group member identifiers
                        ,(Get-FalconHostGroupMember -Id $PSBoundParameters.GroupId -All)
                    }
                } else {
                    # Use provided Host identifiers
                    ,$PSBoundParameters.HostIds
                }
                for ($i = 0; $i -lt ($HostArray | Measure-Object).Count; $i += 1000) {
                    # Create baseline output and define request parameters
                    [array] $Group = Initialize-Output $HostArray[$i..($i + 999)]
                    $InitParam = @{
                        HostIds      = $Group.aid
                        QueueOffline = if ($PSBoundParameters.QueueOffline -eq $true) { $true } else { $false }
                    }
                    # Define command request parameters
                    if ($InvokeCmd -eq 'Invoke-FalconBatchGet') {
                        $CmdParam = @{ FilePath = $PSBoundParameters.Arguments }
                    } else {
                        $CmdParam = @{ Command = $PSBoundParameters.Command }
                        if ($PSBoundParameters.Arguments) {
                            $CmdParam['Arguments'] = $PSBoundParameters.Arguments
                        }
                    }
                    if ($PSBoundParameters.Timeout) {
                        @($InitParam, $CmdParam).foreach{
                            $_['Timeout'] = $PSBoundParameters.Timeout
                        }
                    }
                    # Start session
                    $InitRequest = Start-FalconSession @InitParam
                    if ($InitRequest.batch_id) {
                        # Capture session initialization result
                        $InitResult = Get-RtrResult -Object $InitRequest.hosts -Output $Group
                        @($InitResult | Where-Object { $_.session_id }).foreach{
                            # Add batch_id and clear 'stdout'
                            $_.batch_id = $InitRequest.batch_id
                            $_.stdout = $null
                        }
                        # Perform command request
                        $CmdRequest = & $InvokeCmd @CmdParam -BatchId $InitRequest.batch_id
                        if ($InvokeCmd -eq 'Invoke-FalconBatchGet') {
                            # Capture 'hosts' for 'Invoke-FalconBatchGet'
                            $CmdContent = Get-RtrResult -Object $CmdRequest.hosts -Output $InitResult
                            @($CmdContent | Where-Object { $_.session_id -and $_.complete -eq $true }).foreach{
                                # Add 'batch_get_cmd_req_id' to output
                                Add-Property -Object $_ -Name 'batch_get_cmd_req_id' -Value (
                                    $CmdRequest.batch_get_cmd_req_id)
                            }
                            $CmdContent
                        } else {
                            # Output result
                            Get-RtrResult -Object $CmdRequest -Output $InitResult | ForEach-Object {
                                if ($_.stdout -and $PSBoundParameters.Command -eq 'runscript') {
                                    # Attempt to convert 'stdout' from Json for 'runscript'
                                    $StdOut = try { $_.stdout | ConvertFrom-Json } catch { $null }
                                    if ($StdOut) { $_.stdout = $StdOut }
                                }
                                $_
                            }
                        }
                    }
                }
            }
        } catch {
            throw $_
        }
    }
}