Private/Private.ps1

function Clear-Auth {
    <#
    .SYNOPSIS
        Removes cached authentication and token information
    #>

    [CmdletBinding()]
    [OutputType()]
    param()
    process {
        @('Hostname', 'ClientId', 'ClientSecret', 'MemberCid', 'Token').foreach{
            if ($Falcon.$_) {
                $Falcon.$_ = $null
            }
        }
        $Falcon.Expires = Get-Date
    }
}
function Format-Body {
    <#
    .SYNOPSIS
        Converts a 'splat' hashtable body from Get-Param into Json
    .PARAMETER PARAM
        Parameter hashtable
    #>

    [CmdletBinding()]
    [OutputType()]
    param(
        [Parameter(Mandatory = $true)]
        [hashtable] $Param
    )
    process {
        if ($Param.Body -and ($Falcon.GetEndpoint($Param.Endpoint).consumes -eq 'application/json')) {
            # Check 'consumes' value for endpoint and convert body values to Json
            $Param.Body = ConvertTo-Json $Param.Body -Depth 8
            Write-Debug "[$($MyInvocation.MyCommand.Name)] $($Param.Body)"
        }
    }
}
function Format-Header {
    <#
    .SYNOPSIS
        Adds header values to request from endpoint and user input
    .PARAMETER ENDPOINT
        Falcon endpoint
    .PARAMETER REQUEST
        Request object
    .PARAMETER HEADER
        Additional header values to add from user input
    #>

    [CmdletBinding()]
    [OutputType()]
    param(
        [Parameter(Mandatory = $true)]
        [hashtable] $Endpoint,

        [Parameter(Mandatory = $true)]
        [object] $Request,

        [Parameter()]
        [hashtable] $Header
    )
    begin {
        $Authorization = if ($Endpoint.security -match ".*:(read|write)") {
            # Capture cached token value
            $Falcon.token
        } else {
            # Get basic authorization value
            Get-AuthPair
        }
    }
    process {
        if ($Endpoint.consumes) {
            # Add 'consumes' values as 'Content-Type'
            $Request.Headers.Add('ContentType', $Endpoint.consumes)
        }
        if ($Endpoint.produces) {
            # Add 'produces' values as 'Accept'
            $Request.Headers.Add('Accept', $Endpoint.produces)
        }
        if ($Header) {
            foreach ($Pair in $Header.GetEnumerator()) {
                # Add additional header inputs
                $Request.Headers.Add($Pair.Key, $Pair.Value)
            }
        }
        if ($Authorization) {
            # Add authorization
            $Request.Headers.Add('Authorization', $Authorization)
        }
        # Output debug
        $DebugHeader = ($Request.Headers.GetEnumerator()).Where({ $_.Key -NE 'Authorization' }).foreach{
            "$($_.Key): '$($_.Value)'" } -join ', '
        Write-Debug "[$($MyInvocation.MyCommand.Name)] $DebugHeader"
    }
}
function Format-Result {
    <#
    .SYNOPSIS
        Flattens and formats a response from the Falcon API
    .PARAMETER RESPONSE
        Response object from a Falcon API request
    .PARAMETER ENDPOINT
        Falcon endpoint
    #>

    [CmdletBinding()]
    [OutputType()]
    param(
        [Parameter(Mandatory = $true)]
        [object] $Response,

        [Parameter(Mandatory = $true)]
        [string] $Endpoint
    )
    begin {
        # Capture StatusCode from response
        $StatusCode = $Response.Result.StatusCode.GetHashCode()
        $Schema = if ($StatusCode) {
            # Determine 'schema' type from StatusCode
            $Falcon.GetResponse($Endpoint, $StatusCode)
        }
        if ($Response.Result.Content -match '^<') {
            # Output HTML responses as plain strings
            try {
                $HTML = ($Response.Result.Content).ReadAsStringAsync().Result
            } catch {
                Write-Error $_
            }
        } elseif ($Response.Result.Content) {
            # Convert Json responses into PowerShell objects
            try {
                $Json = ConvertFrom-Json ($Response.Result.Content).ReadAsStringAsync().Result
            } catch {
                Write-Error $_
            }
        }
        if ($Json) {
            # Capture 'meta' information to private variable for processing with Invoke-Loop
            Read-Meta -Object $Json -Endpoint $Endpoint -TypeName $Schema
            Write-Debug "[$($MyInvocation.MyCommand.Name)] `r`n$($Json | ConvertTo-Json -Depth 16)"
        }
    }
    process {
        try {
            if ($Json) {
                # Count populated sub-objects in API response
                $Populated = ($Json.PSObject.Properties).Where({ ($_.Name -ne 'meta') -and
                ($_.Name -ne 'errors') }).foreach{
                    if ($_.Value) {
                        $_.Name
                    }
                }
                ($Json.PSObject.Properties).Where({ ($_.Name -eq 'errors') }).foreach{
                    if ($_.Value) {
                        ($_.Value).foreach{
                            $PSCmdlet.WriteError(
                                [System.Management.Automation.ErrorRecord]::New(
                                    [Exception]::New("$($_.code): $($_.message)"),
                                    $Meta.trace_id,
                                    [System.Management.Automation.ErrorCategory]::NotSpecified,
                                    $Response.Result
                                )
                            )
                        }
                    }
                }
                # Format response to output only relevant fields, instead of entire object
                $Output = if ($Populated.count -gt 1) {
                    # For Real-time Response batch session creation, create custom object
                    if ($Populated -eq 'batch_id' -and 'resources') {
                        [PSCustomObject] @{
                            batch_id = $Json.batch_id
                            hosts = $Json.resources.PSObject.Properties.Value
                        }
                    } else {
                        # Output undefined sub-objects
                        $Json
                    }
                }
                elseif ($Populated.count -eq 1) {
                    if ($Populated[0] -eq 'combined') {
                        # If 'combined', return the results under combined
                        $Json.combined.resources.PSObject.Properties.Value
                    } else {
                        # Output sub-object
                        $Json.($Populated[0])
                    }
                }
                else {
                    if ($Meta) {
                        ($Meta.PSObject.Properties.Name).foreach{
                            # Output fields from 'meta' that aren't pagination/diagnostic related
                            if ($_ -notmatch '(entity|pagination|powered_by|query_time|trace_id)' -and $Meta.$_) {
                                if (-not($MetaValues)) {
                                    $MetaValues = [PSCustomObject] @{}
                                }
                                $Name = if ($_ -eq 'writes') {
                                    $Meta.$_.PSObject.Properties.Name
                                } else {
                                    $_
                                }
                                $Value = if ($Name -eq 'resources_affected') {
                                    $Meta.$_.PSObject.Properties.Value
                                } else {
                                    $Meta.$_
                                }
                                $MetaValues.PSObject.Properties.Add((New-Object PSNoteProperty($Name,$Value)))
                            }
                        }
                        if ($MetaValues) {
                            # Output meta values
                            $MetaValues
                        }
                    }
                }
                if ($Output) {
                    # Output formatted result
                    $Output
                }
            } elseif ($HTML) {
                # Output HTML
                $HTML
            } elseif ($Response.Result.Content) {
                # If unable to convert HTML or Json, output as-is
                ($Response.Result.Content).ReadAsStringAsync().Result
            } else {
                # Output request error
                $Response.Result.EnsureSuccessStatusCode()
            }
        } catch {
            # Output exception
            throw $_
        }
    }
}
function Get-AuthPair {
    <#
    .SYNOPSIS
        Outputs a base64 authorization pair for Format-Header
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param()
    process {
        if ($Falcon.ClientId -and $Falcon.ClientSecret) {
            # Convert cached ClientId/ClientSecret to Base64 for basic auth requests
            "basic $([System.Convert]::ToBase64String(
                [System.Text.Encoding]::ASCII.GetBytes("$($Falcon.ClientId):$($Falcon.ClientSecret)")))"

        } else {
            $null
        }
    }
}
function Get-Body {
    <#
    .SYNOPSIS
        Outputs body parameters from input
    .PARAMETER ENDPOINT
        Falcon endpoint
    .PARAMETER DYNAMIC
        A runtime parameter dictionary to search for input values
    #>

    [CmdletBinding()]
    [OutputType()]
    param(
        [Parameter(Mandatory = $true)]
        [hashtable] $Endpoint,

        [Parameter(Mandatory = $true)]
        [System.Collections.ArrayList] $Dynamic
    )
    begin {
        if ($PSVersionTable.PSVersion.Major -lt 6) {
            Add-Type -AssemblyName System.Net.Http
        }
    }
    process {
        foreach ($Item in $Dynamic.Values.Where({ $_.IsSet -eq $true })) {
            # Match dynamic parameters to parameters defined by endpoint
            $Endpoint.parameters.GetEnumerator().Where({ ($_.Value.dynamic -eq $Item.Name) -and
            ((-not $_.Value.in) -or ($_.Value.in -eq 'body')) -and ($_.Value.type -ne 'switch') }).foreach{
                if ($_.Key -eq 'body') {
                    # Convert files sent as 'body' to ByteStream and upload
                    $ByteStream = if ($PSVersionTable.PSVersion.Major -ge 6) {
                        Get-Content $Item.Value -AsByteStream
                    }
                    else {
                        Get-Content $Item.Value -Encoding Byte -Raw
                    }
                    $ByteArray = [System.Net.Http.ByteArrayContent]::New($ByteStream)
                    $ByteArray.Headers.Add('Content-Type', $Endpoint.consumes)
                    Write-Debug "[Get-Body] File: $($Item.Value)"
                } else {
                    if ($Item.Value | Get-Member -MemberType Method | Where-Object { $_.Name -eq 'Normalize'}) {
                        # Normalize fields created through 'Get-Content' to prevent Json conversion errors
                        if ($Item.ParameterType.Name -eq 'Array') {
                            [array] $Item.Value = ($Item.Value).Normalize()
                        } else {
                            $Item.Value = ($Item.Value).Normalize()
                        }
                        Write-Debug "[Get-Body] Normalized '$($Item.Name)' content"
                    }
                    if (-not($BodyOutput)) {
                        $BodyOutput = @{}
                    }
                    if ($_.Value.parent) {
                        if (-not($Parents)) {
                            # Construct table to hold child input
                            $Parents = @{}
                        }
                        if (-not($Parents.($_.Value.parent))) {
                            $Parents[$_.Value.parent] = @{}
                        }
                        $Parents.($_.Value.parent)[$_.Key] = $Item.Value
                    } else {
                        # Add input to hashtable for Json conversion
                        $BodyOutput[$_.Key] = $Item.Value
                    }
                }
            }
        }
        if ($Parents) {
            $Parents.GetEnumerator().foreach{
                # Add "Parent" object as array to body
                $BodyOutput[$_.Key] = @( $_.Value )
            }
        }
        if ($BodyOutput) {
            # Output body table
            $BodyOutput
        } elseif ($ByteArray) {
            # Output ByteStream
            $ByteArray
        }
    }
}
function Get-Dictionary {
    <#
    .SYNOPSIS
        Creates a dynamic parameter dictionary
    .PARAMETER ENDPOINTS
        An array of 'path:method' endpoint values
    #>

    [CmdletBinding()]
    [OutputType([System.Management.Automation.RuntimeDefinedParameterDictionary])]
    param(
        [Parameter(Mandatory = $true)]
        [array] $Endpoints
    )
    begin {
        # Create parameter dictionary
        $Output = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
        function Add-Parameter ($Parameter) {
            ($Parameter.GetEnumerator()).foreach{
                # Create parameters defined by endpoint
                $Attribute = New-Object System.Management.Automation.ParameterAttribute
                $Attribute.ParameterSetName = $_.Value.set
                $Attribute.Mandatory = $_.Value.required
                if ($_.Value.description) {
                    $Attribute.HelpMessage = $_.Value.description
                }
                if ($_.Value.position) {
                    $Attribute.Position = $_.Value.position
                }
                if ($_.Value.pipeline) {
                    $Attribute.ValueFromPipeline = $_.Value.pipeline
                }
                if ($Output.($_.Value.dynamic)) {
                    $Output.($_.Value.dynamic).Attributes.add($Attribute)
                } else {
                    $Collection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
                    $Collection.Add($Attribute)
                    $PSType = switch ($_.Value.type) {
                        'array' { [array] }
                        'boolean' { [bool] }
                        'double' { [double] }
                        'integer' { [int] }
                        'int32' { [Int32] }
                        'int64' { [Int64] }
                        'object' { [object] }
                        'switch' { [switch] }
                        default { [string] }
                    }
                    if ($_.Value.required -eq $false) {
                        $Collection.Add((New-Object Management.Automation.ValidateNotNullOrEmptyAttribute))
                    }
                    if ($_.Value.enum) {
                        $ValidSet = New-Object System.Management.Automation.ValidateSetAttribute($_.Value.enum)
                        $ValidSet.IgnoreCase = $false
                        $Collection.Add($ValidSet)
                    }
                    if ($_.Value.min -and $_.Value.max) {
                        if ($PSType -eq [int]) {
                            # Set range min/max for integers
                            $Collection.Add((New-Object Management.Automation.ValidateRangeAttribute(
                                $_.Value.Min, $_.Value.Max)))
                        } elseif ($PSType -eq [string]) {
                            # Set length min/max for strings
                            $Collection.Add((New-Object Management.Automation.ValidateLengthAttribute(
                                    $_.Value.Min, $_.Value.Max)))
                        }
                    }
                    if ($_.Value.pattern) {
                        # Set RegEx validation pattern
                        $Collection.Add((New-Object Management.Automation.ValidatePatternAttribute(
                            ($_.Value.pattern).ToString())))
                    }
                    if ($_.Value.script) {
                        # Set ValidationScript
                        $ValidScript = New-Object Management.Automation.ValidateScriptAttribute(
                            [scriptblock]::Create($_.Value.script))
                        if ($_.Value.scripterror -and $ValidScript.ErrorMessage) {
                            $ValidScript.ErrorMessage = $_.Value.scripterror
                        }
                        $Collection.Add($ValidScript)
                    }
                    # Add parameter to dictionary
                    $RunParam = New-Object System.Management.Automation.RuntimeDefinedParameter(
                        $_.Value.dynamic, $PSType, $Collection)
                    $Output.Add($_.Value.dynamic, $RunParam)
                }
            }
        }
    }
    process {
        ($Endpoints).foreach{
            ($Falcon.GetEndpoint($_).Parameters).foreach{
                # Add parameters from each endpoint
                Add-Parameter -Parameter $_
            }
        }
        ($Endpoints -match '(/combined/|/queries/)').foreach{
            if ($Endpoints -match '(/entities/|/combined/)') {
                # Add 'Detailed' parameter when both 'queries' and 'entities/combined' endpoints are present
                Add-Parameter @{
                    detailed = @{
                        dynamic = 'Detailed'
                        set = $_
                        type = 'switch'
                        description = 'Retrieve detailed information'
                    }
                }
            }
            if ($Output.Offset -or $Output.After) {
                # Add 'All' switch when using a 'queries' endpoint that has pagination parameters
                Add-Parameter @{
                    all = @{
                        dynamic = 'All'
                        set = $_
                        type = 'switch'
                        description = 'Repeat requests until all available results are retrieved'
                    }
                }
                # Add 'Total' switch
                Add-Parameter @{
                    total = @{
                        dynamic = 'Total'
                        set = $_
                        type = 'switch'
                        description = 'Display total result count instead of results'
                    }
                }
            }
        }
        # Add 'Help' to all endpoints
        Add-Parameter @{
            help = @{
                dynamic = 'Help'
                set = 'psfalcon:help'
                type = 'switch'
                required = $true
                description = 'Output dynamic help information'
            }
        }
        # Output dictionary
        return $Output
    }
}
function Get-DynamicHelp {
    <#
    .SYNOPSIS
        Outputs basic information about dynamic parameters
    .PARAMETER COMMAND
        PSFalcon command name(s)
    .PARAMETER EXCLUSIONS
        Endpoints to exclude from results (for redundancies)
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory = $true)]
        [string] $Command,

        [Parameter()]
        [array] $Exclusions
    )
    begin {
        function Show-Parameter ($Parameter) {
            # Output name and type
            $Type = if ($_.Value.type) {
                $_.Value.type
            } else {
                'string'
            }
            $Label = "`n -$($_.Value.dynamic) [$($Type)]"
            if ($_.Value.required -eq $true) {
                # Output required status
                $Label += " <Required>"
            }
            # Output description
            $Label + "`n $($_.Value.description)"
            (($_.Value).GetEnumerator().Where({ $_.Key -match '(enum|min|max|pattern|position)' }) |
            Sort-Object { $_.Key }).foreach{
                $Value = if ($_.Value -is [array]) {
                    # Convert arrays to strings
                    $_.Value -join ', '
                } else {
                    $_.Value
                }
                # Output remaining properties
                " $($Falcon.Culture.ToTitleCase($_.Key)) : $Value"
            }
        }
    }
    process {
        # Gather endpoint names from $Command
        ((Get-Command $Command).ParameterSets.Where({ ($_.Name -ne 'psfalcon:help') -and
        ($Exclusions -notcontains $_.Name) })).foreach{
            $Ref = $Falcon.GetEndpoint($_.Name)
            # Output endpoint description and permission
            "`n# $($Ref.description)"
            if ($Ref.security) {
                " Requires $($Ref.security)"
            }
            if ($Ref.parameters) {
                (($Ref.parameters).GetEnumerator().Where({ $_.Value.type -ne 'switch' }) |
                Sort-Object { $_.Value.position }).foreach{
                    # Output parameters from endpoint based on position
                    Show-Parameter -Parameter $_
                }
                ($Ref.Parameters).GetEnumerator().Where({ $_.Value.type -eq 'switch' }).foreach{
                    # Output switch parameters from endpoint
                    Show-Parameter -Parameter $_
                }
            }
            ($_.Parameters).Where({ $_.Name -match '^(All|Detailed|Total)$'}).foreach{
                # Show switch parameters added by Get-Dictionary
                "`n -$($_.Name) [switch]`n $($_.HelpMessage)"
            }
        }
        "`n"
    }
}
function Get-Formdata {
    <#
    .SYNOPSIS
        Outputs 'Formdata' dictionary from input
    .PARAMETER ENDPOINT
        Falcon endpoint
    .PARAMETER DYNAMIC
        A runtime parameter dictionary to search for input values
    #>

    [CmdletBinding()]
    [OutputType()]
    param(
        [Parameter(Mandatory = $true)]
        [hashtable] $Endpoint,

        [Parameter(Mandatory = $true)]
        [System.Collections.ArrayList] $Dynamic
    )
    process {
        foreach ($Item in $Dynamic.Values.Where({ $_.IsSet -eq $true })) {
            # Match dynamic parameters to parameters defined by endpoint
            $Endpoint.parameters.GetEnumerator().Where({ ($_.Value.dynamic -eq $Item.Name) -and
            ($_.Value.In -eq 'formdata') }).foreach{
                # Construct formdata table
                if (-not($FormdataOutput)) {
                    $FormdataOutput = @{}
                }
                $Value = if ($_.Key -eq 'content') {
                    # Collect file content as a string
                    [string] (Get-Content $Item.Value -Raw)
                } else {
                    $Item.Value
                }
                $FormdataOutput[$_.Key] = $Value
            }
        }
        if ($FormdataOutput) {
            # Output formdata table
            Write-Debug "[$($MyInvocation.MyCommand.Name)] $(ConvertTo-Json $FormdataOutput)"
            $FormdataOutput
        }
    }
}
function Get-Header {
    <#
    .SYNOPSIS
        Outputs a hashtable of header values from input
    .PARAMETER ENDPOINT
        Falcon endpoint
    .PARAMETER DYNAMIC
        A runtime parameter dictionary to search for input values
    #>

    [CmdletBinding()]
    [OutputType()]
    param(
        [Parameter(Mandatory = $true)]
        [hashtable] $Endpoint,

        [Parameter(Mandatory = $true)]
        [System.Collections.ArrayList] $Dynamic
    )
    process {
        foreach ($Item in $Dynamic.Values.Where({ $_.IsSet -eq $true })) {
            # Match dynamic parameters to parameters defined by endpoint
            $Endpoint.parameters.GetEnumerator().Where({ ($_.Value.dynamic -eq $Item.Name) -and
            ($_.Value.In -eq 'header') }).foreach{
                # Construct header table
                if (-not($HeaderOutput)) {
                    $HeaderOutput = @{}
                }
                $HeaderOutput[$_.Key] = $Item.Value
            }
        }
        if ($HeaderOutput) {
            # Output header table
            $HeaderOutput
        }
    }
}
function Get-LoopParam {
    <#
    .SYNOPSIS
        Creates a 'splat' hashtable for Invoke-Loop
    .PARAMETER DYNAMIC
        A runtime parameter dictionary to search for input values
    #>

    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory = $true)]
        [System.Collections.ArrayList] $Dynamic
    )
    begin {
        $Output = @{}
    }
    process {
        foreach ($Item in ($Dynamic.Values).Where({ ($_.IsSet -eq $true) -and
        ($_.Name -notmatch '(offset|after|all|detailed)') })) {
            # Add dynamic inputs, but exclude parameters that will break Invoke-Loop
            $Output[$Item.Name] = $Item.Value
        }
        $Output
    }
}
function Get-Outfile {
    <#
    .SYNOPSIS
        Corrects relative user path inputs for 'outfile' content
    .PARAMETER ENDPOINT
        Falcon endpoint
    .PARAMETER DYNAMIC
        A runtime parameter dictionary to search for input values
    #>

    [CmdletBinding()]
    [OutputType()]
    param(
        [Parameter(Mandatory = $true)]
        [hashtable] $Endpoint,

        [Parameter(Mandatory = $true)]
        [System.Collections.ArrayList] $Dynamic
    )
    process {
        foreach ($Item in $Dynamic.Values.Where({ $_.IsSet -eq $true })) {
            # Match dynamic parameters to parameters defined by endpoint
            $FileOutput = $Endpoint.parameters.GetEnumerator().Where({ ($_.Value.dynamic -eq $Item.Name) -and
            ($_.Value.In -eq 'outfile') }).foreach{
                # Convert relative paths
                $Falcon.GetAbsolutePath($Item.Value)
            }
            if ($FileOutput) {
                # Output file path string
                Write-Debug "[$($MyInvocation.MyCommand.Name)] $FileOutput"
                $FileOutput
            }
        }
    }
}
function Get-Param {
    <#
    .SYNOPSIS
        Creates a 'splat' hashtable for Invoke-Endpoint
    .PARAMETER ENDPOINT
        Falcon endpoint name
    .PARAMETER DYNAMIC
        A runtime parameter dictionary to search for input values
    .PARAMETER MAX
        A maximum number of identifiers per request
    #>

    [CmdletBinding()]
    [OutputType()]
    param(
        [Parameter(Mandatory = $true)]
        [string] $Endpoint,

        [Parameter(Mandatory = $true)]
        [System.Collections.ArrayList] $Dynamic,

        [Parameter()]
        [int] $Max
    )
    begin {
        # Construct output table and gather information about endpoint
        $Output = @{
            Endpoint = $Endpoint
        }
        $Target = $Falcon.GetEndpoint($Endpoint)
    }
    process {
        @('Body', 'Formdata', 'Header', 'Outfile', 'Path', 'Query').foreach{
            # Create key/value pairs for each "Get-<Input>" function
            $Value = & "Get-$_" -Endpoint $Target -Dynamic $Dynamic
            if ($Value) {
                $Output[$_] = $Value
            }
        }
        # Pass parameter sets to Split-Param
        $Param = @{
            Param = $Output
        }
        if ($Max) {
            $Param['Max'] = $Max
        }
        Split-Param @Param
    }
}
function Get-Path {
    <#
    .SYNOPSIS
        Modifies an endpoint 'path' value based on input
    .PARAMETER ENDPOINT
        Falcon endpoint
    .PARAMETER DYNAMIC
        A runtime parameter dictionary to search for input values
    #>

    [CmdletBinding()]
    [OutputType()]
    param(
        [Parameter(Mandatory = $true)]
        [hashtable] $Endpoint,

        [Parameter(Mandatory = $true)]
        [System.Collections.ArrayList] $Dynamic
    )
    begin {
        $PathOutput = $Endpoint.Path
    }
    process {
        foreach ($Item in $Dynamic.Values.Where({ $_.IsSet -eq $true })) {
            # Match dynamic parameters to parameters defined by endpoint
            $PathOutput = $Endpoint.parameters.GetEnumerator().Where({ ($_.Value.dynamic -eq $Item.Name) -and
            ($_.Value.In -eq 'path') }).foreach{
                $Endpoint.path -replace $_.Key, $Item.Value
            }
            if ($PathOutput) {
                # Output new URI path
                Write-Debug "[$($MyInvocation.MyCommand.Name)] $PathOutput"
                $PathOutput
            }
        }
    }
}
function Get-Query {
    <#
    .SYNOPSIS
        Outputs an array of query values from user input
    .PARAMETER ENDPOINT
        Falcon endpoint
    .PARAMETER DYNAMIC
        A runtime parameter dictionary to search for input values
    #>

    [CmdletBinding()]
    [OutputType()]
    param(
        [Parameter(Mandatory = $true)]
        [hashtable] $Endpoint,

        [Parameter(Mandatory = $true)]
        [System.Collections.ArrayList] $Dynamic
    )
    begin {
        # Check for relative "last X days/hours" filter values and convert them to RFC-3339
        if ($Dynamic.Filter.Value) {
            $Relative = "(last (?<Int>\d{1,}) (day[s]?|hour[s]?))"
            if ($Dynamic.Filter.Value -match $Relative) {
                $Dynamic.Filter.Value | Select-String $Relative -AllMatches | ForEach-Object {
                    foreach ($Match in $_.Matches.Value) {
                        [int] $Int = $Match -replace $Relative, '${Int}'
                        if ($Match -match "day") {
                            $Int = $Int * -24
                        } else {
                            $Int = $Int * -1
                        }
                        $Dynamic.Filter.Value = $Dynamic.Filter.Value -replace $Match, $Falcon.Rfc3339($Int)
                    }
                }
            }
        }
    }
    process {
        $QueryOutput = foreach ($Item in ($Dynamic.Values).Where({ $_.IsSet -eq $true })) {
            # Match dynamic parameters to parameters defined by endpoint
            $Endpoint.parameters.GetEnumerator().Where({ ($_.Value.dynamic -eq $Item.Name) -and
            ($_.Value.in -eq 'query') }).foreach{
                foreach ($Value in $Item.Value) {
                    # Output "query" values to an array and encode '+' to ensure filter input integrity
                    if ($_.Key) {
                        if (($Endpoint.path -eq '/indicators/queries/iocs/v1') -and (($_.Key -eq 'type') -or
                        ($_.Key -eq 'value'))) {
                            # Change type/value to types/values for /indicators/queries/iocs/v1:get
                            ,"$($_.Key)s=$($Value -replace '\+','%2B')"
                        } else {
                            ,"$($_.Key)=$($Value -replace '\+','%2B')"
                        }
                    } else {
                        ,"$($Value -replace '\+','%2B')"
                    }
                }
            }
        }
        if ($QueryOutput) {
            # Trim pagination tokens for debug output and output query array
            $DebugOutput = (($QueryOutput).foreach{
                if (($_ -match '^offset=') -and ($_.Length -gt 14)) {
                    "$($_.Substring(0,13))..."
                } elseif (($_ -match '^after=') -and ($_.Length -gt 13)) {
                    "$($_.Substring(0,12))..."
                } else {
                    $_
                }
            }) -join ', '
            Write-Debug "[$($MyInvocation.MyCommand.Name)] $DebugOutput"
            $QueryOutput
        }
    }
}
function Invoke-Endpoint {
    <#
    .SYNOPSIS
        Makes a request to a Falcon API endpoint
    .PARAMETER ENDPOINT
        Falcon endpoint
    .PARAMETER HEADER
        Header key/value pair user input
    .PARAMETER QUERY
        An array of string values to append to the URI path
    .PARAMETER BODY
        User body string input
    .PARAMETER FORMDATA
        Formdata dictionary from user input
    .PARAMETER OUTFILE
        Path for 'outfile' output
    .PARAMETER PATH
        A modified 'path' value to use in place of the endpoint-defined string
    #>

    [CmdletBinding()]
    [OutputType()]
    param(
        [Parameter(Mandatory = $true)]
        [string] $Endpoint,

        [Parameter()]
        [hashtable] $Header,

        [Parameter()]
        [array] $Query,

        [Parameter()]
        [object] $Body,

        [Parameter()]
        [System.Collections.IDictionary] $Formdata,

        [Parameter()]
        [string] $Outfile,

        [Parameter()]
        [string] $Path
    )
    begin {
        if ((-not($Falcon.Token)) -or (($Falcon.Expires) -le (Get-Date).AddSeconds(30)) -and
        ($Endpoint -ne '/oauth2/token:post')) {
            # Check for expired/expiring tokens and force an OAuth2 token request
            Request-FalconToken
        }
        # Gather endpoint data
        $Target = $Falcon.GetEndpoint($Endpoint)
        $FullUri = if ($Path) {
            # Append URI path to Hostname with user input
            "$($Falcon.Hostname)$($Path)"
        } else {
            "$($Falcon.Hostname)$($Target.Path)"
        }
        Write-Verbose "[$($MyInvocation.MyCommand.Name)] $($Target.Method.ToUpper()) $FullUri"
        if ($Query) {
            # Add query inputs
            $FullUri += "?$($Query -join '&')"
        }
    }
    process {
        if (!($FullUri -as [System.Uri]).AbsoluteUri) {
            # Validate URI path
            throw "'$FullUri' is not a valid URL. Verify module installation."
        }
        # Create System.Net.Http base object and append request header
        $Client = [System.Net.Http.HttpClient]::New()
        $Request = [System.Net.Http.HttpRequestMessage]::New($Target.Method.ToUpper(), [System.Uri]::New($FullUri))
        $Param = @{
            Endpoint = $Target
            Request = $Request
        }
        if ($Header) {
            $Param['Header'] = $Header
        }
        Format-Header @Param
        if ($Query -match 'timeout') {
            # Add timeout value to request if found in query inputs from Real-time Response commands
            $Timeout = [int] (($Query).Where({ $_ -match 'timeout' })).Split('=')[1] + 5
            $Client.Timeout = (New-TimeSpan -Seconds $Timeout).Ticks
            Write-Debug ("[$($MyInvocation.MyCommand.Name)] HttpClient timeout set to $($Timeout) seconds")
        }
        try {
            if ($Formdata) {
                # Create formdata object
                $MultiContent = [System.Net.Http.MultipartFormDataContent]::New()
                foreach ($Key in $Formdata.Keys) {
                    if ($Key -match '(file|upfile)') {
                        # Append files defined by dynamic parameters
                        $FileStream = [System.IO.FileStream]::New($Formdata.$Key, [System.IO.FileMode]::Open)
                        $Filename = [System.IO.Path]::GetFileName($Formdata.$Key)
                        $StreamContent = [System.Net.Http.StreamContent]::New($FileStream)
                        $MultiContent.Add($StreamContent, $Key, $Filename)
                    } else {
                        # Add content as strings
                        $StringContent = [System.Net.Http.StringContent]::New($Formdata.$Key)
                        $MultiContent.Add($StringContent, $Key)
                    }
                }
                # Append formdata object to request
                $Request.Content = $MultiContent
            } elseif ($Body) {
                $Request.Content = if ($Body -is [string]) {
                    # Append Json body to request using endpoint's 'consumes' value
                    [System.Net.Http.StringContent]::New($Body, [System.Text.Encoding]::UTF8, $Target.consumes)
                } else {
                    # Append body to request directly
                    $Body
                }
            }
            $Response = if ($Outfile) {
                # Add 'outfile' to header and receive payload
                ($Request.Headers.GetEnumerator()).foreach{
                    $Client.DefaultRequestHeaders.Add($_.Key, $_.Value)
                }
                $Request.Dispose()
                $Client.GetByteArrayAsync($FullUri)
            } else {
                # Make request
                $Client.SendAsync($Request)
            }
            if ($Response.Result -is [System.Byte[]]) {
                # Write file payload to 'outfile' path
                [System.IO.File]::WriteAllBytes($Outfile, ($Response.Result))
                if (Test-Path $Outfile) {
                    Get-ChildItem $Outfile | Out-Host
                }
            } elseif ($Response.Result) {
                # Format responses
                Format-Result -Response $Response -Endpoint $Endpoint
            } else {
                # Output error
                $PSCmdlet.WriteError(
                    [System.Management.Automation.ErrorRecord]::New(
                        [Exception]::New("Unable to contact $($Falcon.Hostname)"),
                        "psfalcon_connection_failure",
                        [System.Management.Automation.ErrorCategory]::ConnectionError,
                        $Response
                    )
                )
            }
        } catch {
            # Output exception
            throw $_
        }
    }
    end {
        if ($Response.Result.Headers) {
            # Wait for 'X-Ratelimit-RetryAfter'
            Wait-RetryAfter $Response.Result.Headers
        }
        if ($FileStream) {
            $FileStream.Close()
        }
        if ($Response) {
            $Response.Dispose()
        }
    }
}
function Invoke-Loop {
    <#
    .SYNOPSIS
        Watches 'meta' results to repeat command requests
    .PARAMETER COMMAND
        The PSFalcon command to repeat
    .PARAMETER PARAM
        Parameters to include when running the command
    .PARAMETER DETAILED
        Toggle the 'Detailed' switch during command request
    #>

    [CmdletBinding()]
    [OutputType()]
    param(
        [Parameter(Mandatory = $true)]
        [string] $Command,

        [Parameter(Mandatory = $true)]
        [hashtable] $Param,

        [Parameter()]
        [bool] $Detailed
    )
    begin {
        function Get-Paging ($Object, $Param, $Count) {
            # Check 'Meta' object from Format-Result for pagination information
            if ($Object.after) {
                $Param['After'] = $Object.after
            } else {
                if ($Object.next_page) {
                    $Param['Offset'] = $Object.offset
                } else {
                    $Param['Offset'] = if ($Object.offset -match '^\d{1,}$') {
                        $Count
                    } else {
                        $Object.offset
                    }
                }
            }
        }
        if (!$Param.Limit) {
            $Endpoint = if ((Get-Command $Command).ParameterSets.Name -match 'combined' -and $Param.Detailed) {
                # Use 'combined' if available and -Detailed was specified
                'combined'
            } else {
                'queries'
            }
            # Check for 'limit' parameter and add maximum limit if not present
            $Param['Limit'] = ((Get-Command $Command).ParameterSets.Where({
                $_.Name -match $Endpoint }).Parameters.Where({ $_.Name -eq 'Limit' }).Attributes.MaxRange |
                Group-Object) | Select-Object -ExpandProperty Name -First 1
            Write-Debug "[$($MyInvocation.MyCommand.Name)] Added maximum 'Limit'"
        }
    }
    process {
        # Perform initial request
        $Loop = @{
            Request = & $Command @Param
            Pagination = $Meta.pagination
        }
        if ($Loop.Request -and $Detailed) {
            # Perform secondary request for identifier detail
            & $Command -Ids $Loop.Request
        } else {
            $Loop.Request
        }
        if ($Loop.Request -and (($Loop.Request.count -lt $Loop.Pagination.total) -or $Loop.Pagination.next_page)) {
            for ($i = $Loop.Request.count; ($Loop.Pagination.next_page -or ($i -lt $Loop.Pagination.total));
            $i += $Loop.Request.count) {
                # Repeat requests if additional results are defined in 'meta'
                Write-Verbose "[$($MyInvocation.MyCommand.Name)] retrieved $i results"
                Get-Paging -Object $Loop.Pagination -Param $Param -Count $i
                $Loop = @{
                    Request = & $Command @Param
                    Pagination = $Meta.pagination
                }
                if ($Loop.Request -and $Detailed) {
                    & $Command -Ids $Loop.Request
                } else {
                    $Loop.Request
                }
            }
        }
    }
}
function Invoke-Request {
    <#
    .SYNOPSIS
        Determines request type and submits to Invoke-Loop or Invoke-Endpoint
    .PARAMETER COMMAND
        PSFalcon command calling Invoke-Request [required for -All and -Detailed]
    .PARAMETER QUERY
        The Falcon endpoint that for 'queries' operations
    .PARAMETER ENTITY
        The Falcon endpoint that for 'entities' operations
    .PARAMETER DYNAMIC
        A runtime parameter dictionary to search for user input values
    .PARAMETER DETAILED
        Toggle the use of 'Detailed' with a command
    .PARAMETER TOTAL
        Toggle the use of 'Total' with a command
    .PARAMETER MODIFIER
        The name of a switch parameter used to modify a command when using Invoke-Loop
    .PARAMETER ALL
        Toggle the use of Invoke-Loop to repeat command requests
    #>

    [CmdletBinding()]
    [OutputType()]
    param(
        [Parameter()]
        [string] $Command,

        [Parameter(Mandatory = $true)]
        [string] $Query,

        [Parameter()]
        [string] $Entity,

        [Parameter(Mandatory = $true)]
        [System.Collections.ArrayList] $Dynamic,

        [Parameter()]
        [bool] $Detailed,

        [Parameter()]
        [bool] $Total,

        [Parameter()]
        [string] $Modifier,

        [Parameter()]
        [switch] $All
    )
    begin {
        # Set base endpoint based on dynamic input
        $Endpoint = if (($Dynamic.Values).Where({ $_.IsSet -eq $true }).Attributes.ParameterSetName -eq $Entity) {
            $Entity
        } else {
            $Query
        }
    }
    process {
        if ($All -and !$Total) {
            # Construct parameters and pass to Invoke-Loop
            $LoopParam = @{
                Command = $Command
                Param = Get-LoopParam -Dynamic $Dynamic
            }
            if ($Endpoint -match '/combined/.*:get$') {
                $LoopParam.Param['Detailed'] = $true
            }
            if ($Detailed) {
                $LoopParam['Detailed'] = $true
            }
            if ($Modifier) {
                $LoopParam.Param[$Modifier] = $true
            }
            Invoke-Loop @LoopParam
        } else {
            foreach ($Param in (Get-Param -Endpoint $Endpoint -Dynamic $Dynamic)) {
                # Format Json body and make request
                Format-Body -Param $Param
                $Request = Invoke-Endpoint @Param
                if ($Request -and $Detailed -and !$Total) {
                    # Make secondary request for detail about identifiers
                    & $Command -Ids $Request
                } elseif ($Request -and $Total) {
                    # Output total result count
                    $Meta.pagination.total
                } else {
                    $Request
                }
            }
        }
    }
}
function Read-Meta {
    <#
    .SYNOPSIS
        Outputs verbose 'meta' information and creates $Script:Meta for loop processing
    .PARAMETER OBJECT
        Object from a Falcon API request
    .PARAMETER ENDPOINT
        Falcon endpoint
    .PARAMETER TYPENAME
        Optional 'meta' object typename, sourced from API response code/definition
    #>

    [CmdletBinding()]
    [OutputType()]
    param(
        [Parameter(Mandatory = $true)]
        [object] $Object,

        [Parameter(Mandatory = $true)]
        [string] $Endpoint,

        [Parameter()]
        [string] $TypeName
    )
    begin {
        function Read-CountValue ($Property, $Prefix) {
            # Output 'meta' values
            if ($_.Value -is [PSCustomObject]) {
                $ItemPrefix = $_.Name
                ($_.Value.PSObject.Properties).foreach{
                    Read-CountValue -Property $_ -Prefix $ItemPrefix
                }
            } elseif ($_.Name -match '(after|offset|total)') {
                $Value = if (($_.Value -is [string]) -and ($_.Value.Length -gt 7)) {
                    "$($_.Value.Substring(0,6))..."
                } else {
                    $_.Value
                }
                $Name = if ($Prefix) {
                    "$($Prefix)_$($_.Name)"
                } else {
                    $_.Name
                }
                if ($Name -and $Value) {
                    "$($Name): $($Value)"
                }
            }
        }
    }
    process {
        Write-Debug "[$($MyInvocation.MyCommand.Name)] $($StatusCode): $TypeName"
        if ($Object.meta) {
            # Create script 'meta' variable for internal reference
            $Script:Meta = $Object.meta
            if ($TypeName) {
                # Set object typename to 'schema' from response
                $Meta.PSObject.TypeNames.Insert(0,$TypeName)
            }
        }
        if ($Meta) {
            if ($Meta.trace_id) {
                # Output trace_id
                Write-Verbose "[$($MyInvocation.MyCommand.Name)] trace_id: $($Meta.trace_id)"
            }
            $CountInfo = (($Meta.PSObject.Properties).foreach{
                # Output pagination
                Read-CountValue $_
            }) -join ', '
            if ($CountInfo) {
                Write-Verbose "[$($MyInvocation.MyCommand.Name)] $CountInfo"
            }
        }
    }
}
function Split-Param {
    <#
    .SYNOPSIS
        Splits 'splat' hashtables into smaller groups to avoid API limitations
    .PARAMETER PARAM
        Parameter hashtable
    .PARAMETER MAX
        A manually-defined maximum number of identifiers per request
    #>

    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory = $true)]
        [hashtable] $Param,

        [Parameter()]
        [int] $Max
    )
    begin {
        if (-not($Max)) {
            # Gather endpoint information
            $Endpoint = $Falcon.GetEndpoint($Param.Endpoint)
            $Max = if ($Output.Query -match 'ids=') {
                # Calculate URL length based on hostname, endpoint path and input
                $PathLength = ("$($Falcon.Hostname)$($Endpoint.Path)").Length
                $LongestId = (($Output.Query).Where({ $_ -match 'ids='}) |
                    Measure-Object -Maximum -Property Length).Maximum + 1
                $IdCount = [Math]::Floor([decimal]((65535 - $PathLength)/$LongestId))
                if ($IdCount -gt 500) {
                    # Set maximum for requests to 500
                    500
                } else {
                    # Use maximum below 500
                    $IdCount
                }
            } elseif ($Endpoint.parameters -and ($Endpoint.Parameters.GetEnumerator().Where({
            $_.Key -eq 'ids' }).Value.max -gt 0)) {
                # Use maximum defined by endpoint
                $Endpoint.parameters.GetEnumerator().Where({ $_.Key -eq 'ids' }).Value.max
            } else {
                $null
            }
        }
    }
    process {
        if ($Max -and $Param.Query.count -gt $Max) {
            Write-Debug "[$($MyInvocation.MyCommand.Name)] $Max query values per request"
            for ($i = 0; $i -lt $Param.Query.count; $i += $Max) {
                # Break query inputs into groups that are lower than maximum
                $Group = @{
                    Query = $Param.Query[$i..($i + ($Max - 1))]
                }
                ($Param.Keys).foreach{
                    if ($_ -ne 'Query') {
                        $Group[$_] = $Param.$_
                    }
                }
                $Group
            }
        } elseif ($Max -and $Param.Body.ids.count -gt $Max) {
            Write-Debug "[$($MyInvocation.MyCommand.Name)] $Max body values per request"
            for ($i = 0; $i -lt $Param.Body.ids.count; $i += $Max) {
                # Break body inputs into groups that are lower than maximum
                $Group = @{
                    Body = @{
                        ids = $Param.Body.ids[$i..($i + ($Max - 1))]
                    }
                }
                ($Param.Keys).foreach{
                    if ($_ -ne 'Body') {
                        $Group[$_] = $Param.$_
                    } else {
                        (($Param.$_).Keys).foreach{
                            if ($_ -ne 'ids') {
                                $Group.Body[$_] = $Param.Body.$_
                            }
                        }
                    }
                }
                $Group
            }
        } else {
            # If maximum is not exceeded, output as-is
            $Param
        }
    }
}
function Wait-RetryAfter {
    <#
    .SYNOPSIS
        Checks a Falcon API response for rate limiting and waits
    .PARAMETER HEADERS
        Response headers from Falcon endpoint
    #>

    [CmdletBinding()]
    [OutputType()]
    param(
        [Parameter(Mandatory = $true)]
        [object] $Headers
    )
    process {
        if ($Headers.Key -contains 'X-Ratelimit-RetryAfter') {
            # Determine wait time from response header and sleep
            $RetryAfter = (($Headers.GetEnumerator()).Where({ $_.Key -eq 'X-Ratelimit-RetryAfter' })).Value
            $Wait = ($RetryAfter - ([int] (Get-Date -UFormat %s) + 1))
            Write-Verbose "[$($MyInvocation.MyCommand.Name)] rate limited for $Wait seconds"
            Start-Sleep -Seconds $Wait
        }
    }
}