ConnectWiser.psm1

function Get-CwOption {
    [CmdletBinding()]
    param()
    $script:CwOption
}
function Get-CWCAuditInfo {
    [CmdletBinding()]
    param ()

    $Endpoint = 'Services/AuditService.ashx/GetAuditInfo'

    $WebRequestArguments = @{
        Endpoint = $Endpoint
        Method   = 'Post'
    }

    Invoke-CWCWebRequest -Arguments $WebRequestArguments
}
function Get-CWCAuditLog {
    [CmdletBinding()]
    param (
        [datetime]$StartDate,
        [datetime]$EndDate,
        [string]$SessionName,
        [switch]$IncludeSessionCaptures,
        [int[]]$EventTypes,
        [int[]]$SecurityTypes
    )

    $Endpoint = 'Services/AuditService.ashx/QueryAuditLog'

    $Body = ConvertTo-Json @(
        $(Get-Date $StartDate -Format 'yyyy-MM-ddTHH:mm:ss.ffffZ'),
        $(Get-Date $EndDate -Format 'yyyy-MM-ddTHH:mm:ss.ffffZ'),
        $SessionName,
        $IncludeSessionCaptures.IsPresent
        $EventTypes,
        $SecurityTypes
    )
    Write-Verbose $Body

    $WebRequestArguments = @{
        Endpoint = $Endpoint
        Body     = $Body
        Method   = 'Post'
    }

    Invoke-CWCWebRequest -Arguments $WebRequestArguments
}
function Connect-CWC {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Server,
        [Parameter(Mandatory = $True)]
        [pscredential]$Credentials,
        [switch]$Force,
        [switch]$DisableSessionTrust,
        [string]$OtpCode = $null
    )

    if ($script:CWCServerConnection -and !$Force) {
        Write-Verbose "Using cached Authentication information."
        return
    }

    $Server = $Server -replace ("http.*:\/\/", '')
    $Headers = @{
        'content-type' = "application/json; charset=utf-8"
        'origin'       = "https://$Server"
    }

    $frontPage_param = @{
        Uri             = "https://$Server/Login"
        Headers         = $Headers
        UseBasicParsing = $true
    }
    if ($script:CWCServerConnection.WebSession) { $frontPage_param.WebSession = $script:CWCServerConnection.WebSession }
    else { $frontPage_param.SessionVariable = "session" }
    $FrontPage = Invoke-WebRequest @frontPage_param

    $Regex = [Regex]'(?<=antiForgeryToken":")(.*)(?=","isUserAdministrator)'
    $Match = $Regex.Match($FrontPage.content)
    if ($Match.Success) { $Headers.'x-anti-forgery-token' = $Match.Value.ToString() }
    else { Write-Verbose 'Unable to find anti forgery token. Some commands may not work.' }

    # Each login session has to keep its own consistent nonce between retries so that the server can keep them straight.
    # Remove this GUID and OTP will never work.
    $trackingGuid = [guid]::NewGuid().ToString()
    # Setting a sane default: Everyone loves to have fewer MFA prompts.
    if ($DisableSessionTrust) { $sessionTrust = $false }
    else { $sessionTrust = $true }
    do {
        $response = Invoke-RestMethod "https://$Server/Services/AuthenticationService.ashx/TryLogin" -WebSession $session -Body (@(
                $Credentials.UserName
                $Credentials.GetNetworkCredential().Password
                $OtpCode
                $sessionTrust
                $trackingGuid
            ) | ConvertTo-Json) -ContentType application/json -Method Post
        Write-Verbose "Response from server '$response'"
        if ($response -ne 1) {
            $OtpCode = Read-Host -Prompt "Please enter your OTP code"
        }
    } until ($response -eq 1)

    $script:CwOption.WebSession = $session
    $script:CWCServerConnection = @{
        Server     = $Server
        WebSession = $session
    }
    Write-Verbose ($script:CWCServerConnection | Out-String)

    try {
        $null = Get-CWCSessionGroup -ErrorAction Stop
        Write-Verbose '$CWCServerConnection, variable initialized.'
    }
    catch {
        Remove-Variable CWCServerConnection -Scope script
        Write-Verbose 'Authentication failed.'
        Write-Error $_
    }
}
function Get-CWCLauncURL {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory = $True)]
        [guid[]]$GUID
    )

    $Endpoint = 'App_Extensions/2c4f522f-b39a-413a-8807-dc52a2fce13e/Service.ashx/GetLaunchUrlForSessionId'

    $Body = ConvertTo-Json @(
        @($GUID)
        $onSuccess
        $onFailure
        $userContext
        $userNameOverride
        $passwordOverride
    )
    Write-Verbose $Body

    $WebRequestArguments = @{
        Endpoint = $Endpoint
        Body     = $Body
        Method   = 'Post'
    }
    if ($PSCmdlet.ShouldProcess($WebRequestArguments.Body, 'New-CWCRemoteWorkforceAssignment')) {
        Invoke-CWCWebRequest -Arguments $WebRequestArguments
    }
}
function Add-CWCRemoteWorkforceRequiredRole {
    [CmdletBinding()]
    param()

    $Endpoint = 'Services/SecurityService.ashx/SaveRole'

    $SessionGroups = @('My Assigned Machines')
    $RoleName = 'Remote Workforce'

    $Body = ConvertTo-Json -Depth 10 @(
        "",
        $RoleName,
        @(),
        @(
            @{
                "AccessControlType"  = 0
                "Name"               = "ViewSessionGroup"
                "SessionGroupFilter" = 7
                "SessionGroupPath"   = $SessionGroups
                "OwnershipFilter"    = 0
            },
            @{
                "AccessControlType"  = 0
                "Name"               = "JoinSession"
                "SessionGroupFilter" = 7
                "SessionGroupPath"   = $SessionGroups
                "OwnershipFilter"    = 0
            },
            @{
                "AccessControlType"  = 0
                "Name"               = "HostSessionWithoutConsent"
                "SessionGroupFilter" = 7
                "SessionGroupPath"   = $SessionGroups
                "OwnershipFilter"    = 0
            }
        )
    )
    Write-Verbose $Body

    $WebRequestArguments = @{
        Endpoint = $Endpoint
        Body     = $Body
        Method   = 'Post'
    }
    Invoke-CWCWebRequest -Arguments $WebRequestArguments
}
function New-CWCRemoteWorkforceAssignment {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory = $True)]
        [guid[]]$GUID,
        [Parameter(Mandatory = $True)]
        [string]$Username,
        [string]$DisplayName,
        [string]$Group = 'All Machines'
    )

    $Endpoint = 'App_Extensions/2c4f522f-b39a-413a-8807-dc52a2fce13e/Service.ashx/AddAssignmentNoteToSession'

    $Body = ConvertTo-Json @(
        @($Group),
        $GUID,
        "UserName:$($Username),UserDisplayName:$($DisplayName)"
    )
    Write-Verbose $Body

    $WebRequestArguments = @{
        Endpoint = $Endpoint
        Body     = $Body
        Method   = 'Post'
    }
    if ($PSCmdlet.ShouldProcess($WebRequestArguments.Body, 'New-CWCRemoteWorkforceAssignment')) {
        Invoke-CWCWebRequest -Arguments $WebRequestArguments
    }
}
function New-CWCMFA {
    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'None')]
    param(
        $DisplayName = 'CW Control',
        $UserAccount
    )
    $Possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
    $Key = (1..16 | ForEach-Object { $Possible.ToCharArray() | Get-Random }) -join ''

    $otpauth = "otpauth://totp/$($DisplayName):$($UserAccount)?secret=$($Key)"
    Add-Type -AssemblyName System.Web
    $otpauthEncoded = [System.Web.HTTPUtility]::UrlEncode($otpauth)
    $qrUrl = "https://quickchart.io/chart?cht=qr&chs=400x400&chl=$($otpauthEncoded)&chld=L"
    if ($PSCmdlet.ShouldProcess('New-CWCMFA')) {
        [pscustomobject]@{
            'QR'  = $qrUrl
            'OTP' = "ms:$Key"
        }
    }
}
function Get-CWCLastContact {
    [CmdletBinding()]
    [OutputType([boolean], ParameterSetName = ('Quiet'))]
    [OutputType([datetime])]
    param(
        [Parameter(Mandatory = $True)]
        [guid]$GUID,
        [parameter(ParameterSetName = 'Quiet')]
        [switch]$Quiet,
        [int]$Seconds,
        [string]$Group = 'All Machines'
    )

    # Time conversion
    $origin = New-Object -Type DateTime -ArgumentList 1970, 1, 1, 0, 0, 0, 0
    $epoch = $((New-TimeSpan -Start $(Get-Date -Date '01/01/1970') -End $(Get-Date)).TotalSeconds)

    try { $SessionDetails = Get-CWCSessionDetail -Group $Group -GUID $GUID }
    catch { return $_ }

    if ($SessionDetails -eq 'null' -or !$SessionDetails) {
        Write-Warning 'Machine not found.'
        return $null
    }

    $GuestSessionEvents = $SessionDetails.Events
    $GuestSessionConnections = $SessionDetails.Connections | Where-Object { $_.ParticipantName }

    if ($GuestSessionEvents) {

        # Get connection events
        $LatestEvent = $GuestSessionEvents | Where-Object {
            $_.EventType -in (10, 11) -and
            $_.ConnectionID -NotIn $GuestSessionConnections.ConnectionID
        } | Sort-Object time | Select-Object -First 1
        if ($LatestEvent.EventType -eq 10) {
            # Currently connected
            if ($Quiet) { return $True }
            else { return Get-Date }
        }
        else {
            # Time conversion hell :(
            $TimeDiff = $epoch - ($LatestEvent.Time / 1000)
            $OfflineTime = $origin.AddSeconds($TimeDiff)
            $Difference = New-TimeSpan -Start $OfflineTime -End $(Get-Date)
            if ($Quiet -and $Difference.TotalSeconds -lt $Seconds) { return $True }
            elseif ($Quiet) { return $False }
            else { return $OfflineTime }
        }
    }
    else { return Write-Error 'Unable to determine last contact.' }
}
function Get-CWCSession {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $True)]
        [ValidateSet('Support', 'Access', 'Meeting')]
        $Type,
        [string]$Group = 'All Machines',
        [string]$Search,
        [string]$FindSessionID,
        [int]$Limit
    )

    $Endpoint = 'Services/PageService.ashx/GetLiveData'

    switch ($Type) {
        'Support' { $Number = 0 }
        'Meeting' { $Number = 1 }
        'Access' { $Number = 2 }
        default { return Write-Error "Unknown Type, $Type" }
    }

    $Body = ConvertTo-Json @(
        @{
            HostSessionInfo  = @{
                'sessionType'           = $Number
                'sessionGroupPathParts' = @($Group)
                'filter'                = $Search
                'findSessionID'         = $FindSessionID
                'sessionLimit'          = $Limit
            }
            ActionCenterInfo = @{}
        }
        0
    ) -Depth 5
    Write-Verbose $Body

    $WebRequestArguments = @{
        Endpoint = $Endpoint
        Body     = $Body
        Method   = 'Post'
    }

    $Data = Invoke-CWCWebRequest -Arguments $WebRequestArguments
    $Data.ResponseInfoMap.HostSessionInfo.Sessions
}
function Get-CWCSessionDetail {
    [CmdletBinding()]
    param (
        [string]$Group = 'All Machines',
        [Parameter(Mandatory = $True)]
        [guid]$GUID
    )

    $Endpoint = 'Services/PageService.ashx/GetSessionDetails'

    $Body = ConvertTo-Json @($Group, $GUID)
    Write-Verbose $Body

    $WebRequestArguments = @{
        Endpoint = $Endpoint
        Body     = $Body
        Method   = 'Post'
    }

    Invoke-CWCWebRequest -Arguments $WebRequestArguments
}
function Invoke-CWCCommand {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $True)]
        [guid]$GUID,
        [string]$Command,
        [int]$TimeOut = 10000,
        [int]$MaxLength = 10000,
        [switch]$PowerShell,
        [string]$Group = 'All Machines',
        [switch]$NoWait
    )

    $Endpoint = 'Services/PageService.ashx/AddSessionEvents'

    $origin = New-Object -Type DateTime -ArgumentList 1970, 1, 1, 0, 0, 0, 0
    $SessionEventType = 44

    # Format command
    $FormattedCommand = @()
    if ($Powershell) { $FormattedCommand += '#!ps' }
    $FormattedCommand += "#timeout=$TimeOut"
    $FormattedCommand += "#maxlength=$MaxLength"
    $FormattedCommand += $Command
    $FormattedCommand = $FormattedCommand | Out-String
    $CommandObject = @{
        SessionID = $GUID
        EventType = $SessionEventType
        Data      = $FormattedCommand
    }
    $Body = (ConvertTo-Json @(@($Group), @($CommandObject))).Replace('\r\n', '\n')
    Write-Verbose $Body

    # Issue command
    $WebRequestArguments = @{
        Endpoint = $Endpoint
        Body     = $Body
        Method   = 'Post'
    }
    $null = Invoke-CWCWebRequest -Arguments $WebRequestArguments
    if ($NoWait) { return }

    # Get Session
    try { $SessionDetails = Get-CWCSessionDetail -Group $Group -GUID $GUID }
    catch { return $_ }

    #Get time command was executed
    $epoch = $((New-TimeSpan -Start $(Get-Date -Date '01/01/1970') -End $(Get-Date)).TotalSeconds)
    $ExecuteTime = $epoch - ((($SessionDetails.events | Where-Object { $_.EventType -eq 44 })[-1]).Time / 1000)
    $ExecuteDate = $origin.AddSeconds($ExecuteTime)

    # Look for results of command
    $Looking = $True
    $TimeOutDateTime = (Get-Date).AddMilliseconds($TimeOut)
    $Body = ConvertTo-Json @($Group, $GUID)
    while ($Looking) {
        try { $SessionDetails = Get-CWCSessionDetail -Group $Group -GUID $GUID }
        catch { return $_ }

        $ConnectionsWithData = @()
        Foreach ($Connection in $SessionDetails.Events) {
            $ConnectionsWithData += $Connection | Where-Object { $_.EventType -eq 70 }
        }

        $Events = ($ConnectionsWithData | Where-Object { $_.EventType -eq 70 -and $_.Time })
        foreach ($Event in $Events) {
            $epoch = $((New-TimeSpan -Start $(Get-Date -Date '01/01/1970') -End $(Get-Date)).TotalSeconds)
            $CheckTime = $epoch - ($Event.Time / 1000)
            $CheckDate = $origin.AddSeconds($CheckTime)
            if ($CheckDate -gt $ExecuteDate) {
                $Looking = $False
                $Output = $Event.Data -split '[\r\n]' | Where-Object { $_ -and $_ -ne "C:\WINDOWS\system32>$Command" }
                Write-Verbose $Event.Data
                return $Output
            }
        }

        Start-Sleep -Seconds 1
        if ($(Get-Date) -gt $TimeOutDateTime.AddSeconds(1)) {
            $Looking = $False
            Write-Warning 'Command timed out.'
        }
    }
}
function Invoke-CWCWake {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $True)]
        [guid[]]$GUID,
        [Parameter(Mandatory = $True)]
        [ValidateSet('Support', 'Access')]
        [string]$Type
    )

    $Endpoint = 'Services/PageService.ashx/AddEventToSessions'

    $SessionEventType = 43

    switch ($Type) {
        'Support' { $Group = 'All Sessions' }
        'Access' { $Group = 'All Machines' }
        default { return Write-Error "Unknown Type, $Type" }
    }

    $Body = ConvertTo-Json @($Group, @($GUID), $SessionEventType, '')
    Write-Verbose $Body

    # Issue command
    $WebRequestArguments = @{
        Endpoint = $Endpoint
        Body     = $Body
        Method   = 'Post'
    }

    Invoke-CWCWebRequest -Arguments $WebRequestArguments
}
function New-CWCAccessToken {
    [CmdletBinding()]
    param (
        [String[]]$Group = 'All Machines',
        [Parameter(Mandatory = $True)]
        [guid]$GUID
    )

    $Endpoint = 'Services/PageService.ashx/GetAccessToken'

    $Body = @"
[["$($Group -join '","')"],"$GUID"]
"@

    Write-Verbose $Body

    $WebRequestArguments = @{
        Endpoint = $Endpoint
        Method   = 'Post'
        Body     = $Body
    }
    Invoke-CWCWebRequest -Arguments $WebRequestArguments
}
function Remove-CWCSession {
  [CmdletBinding(SupportsShouldProcess)]
  param (
    [Parameter(Mandatory = $True)]
    [guid[]]$GUID,
    [Parameter(Mandatory = $True)]
    [string]$Group
  )

  $Endpoint = 'Services/PageService.ashx/AddSessionEvents'

  $SessionEventType = 21
  $Body = ConvertTo-Json @(
    @(
      $Group
    ),
    @(
      @{
        SessionID = $GUID
        EventType = $SessionEventType
      }
    )
  )

  $WebRequestArguments = @{
    Endpoint = $Endpoint
    Body     = $Body
    Method   = 'Post'
  }
  if ($PSCmdlet.ShouldProcess($WebRequestArguments.Body, 'Remove-CWCSession')) {
    Invoke-CWCWebRequest -Arguments $WebRequestArguments
  }
}
function Update-CWCCustomProperty {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory = $True)]
        [guid]$GUID,
        [Parameter(Mandatory = $True)]
        [int]$Property,
        [string]$Value,
        [string[]]$Group = 'All Machines'
    )

    $Endpoint = 'Services/PageService.ashx/UpdateSessionCustomPropertyValue'

    $Body = ConvertTo-Json @($Group, $GUID, $Property, $Value)

    $WebRequestArguments = @{
        Endpoint = $Endpoint
        Body     = $Body
        Method   = 'Post'
    }
    if ($PSCmdlet.ShouldProcess($WebRequestArguments.Body, "Update-CWCCustomProperty")) {
        Invoke-CWCWebRequest -Arguments $WebRequestArguments
    }
}
function Update-CWCSessionName {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory = $True)]
        [guid]$GUID,
        [Parameter(Mandatory = $True)]
        [string]$NewName,
        [string]$Group = 'All Machines'
    )

    $Endpoint = 'Services/PageService.ashx/UpdateSessionName'

    $Body = ConvertTo-Json @($Group, $GUID, $NewName)

    $WebRequestArguments = @{
        Endpoint = $Endpoint
        Body     = $Body
        Method   = 'Post'
    }
    if ($PSCmdlet.ShouldProcess($WebRequestArguments.Body, "Update-CWCSessionName")) {
        Invoke-CWCWebRequest -Arguments $WebRequestArguments
    }
}
function Get-CWCSecurityConfigurationInfo {
    [CmdletBinding()]
    param ()

    $Endpoint = 'Services/SecurityService.ashx/GetSecurityConfigurationInfo'

    $WebRequestArguments = @{
        Endpoint = $Endpoint
        Method   = 'Post'
    }
    Invoke-CWCWebRequest -Arguments $WebRequestArguments
}
function New-CWCUser {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory = $True)]
        [pscredential]$Credentials,
        [string]$OTP,
        [string]$DisplayName,
        [Parameter(Mandatory = $True)]
        [string]$Email,
        [string[]]$SecurityGroups = @(),
        [boolean]$ForcePassChange = $true
    )

    $Endpoint = 'Services/SecurityService.ashx/SaveUser'

    $Body = ConvertTo-Json @(
        $script:CwOption.InternalUserSource,
        $null,
        $Credentials.UserName,
        $Credentials.GetNetworkCredential().Password,
        $Credentials.GetNetworkCredential().Password,
        $OTP,
        $DisplayName,
        '',
        $Email,
        $SecurityGroups,
        $ForcePassChange
    )

    $WebRequestArguments = @{
        Endpoint = $Endpoint
        Body     = $Body
        Method   = 'Post'
    }
    if ($PSCmdlet.ShouldProcess($WebRequestArguments.Body, "New-CWCUser")) {
        Invoke-CWCWebRequest -Arguments $WebRequestArguments
    }
}
function Remove-CWCUser {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory = $True)]
        [string]$User
    )

    $Endpoint = 'Services/SecurityService.ashx/DeleteUser'

    $Body = ConvertTo-Json @(
        $script:CwOption.InternalUserSource,
        $User
    )

    $WebRequestArguments = @{
        Endpoint = $Endpoint
        Body     = $Body
        Method   = 'Post'
    }
    if ($PSCmdlet.ShouldProcess($WebRequestArguments.Body, "Remove-CWCUser")) {
        Invoke-CWCWebRequest -Arguments $WebRequestArguments
    }
}
function Update-CWCUser {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory = $True)]
        [string]$UserToUpdate,
        [string]$NewUserName,
        [secureString]$Password,
        [string]$OTP,
        [string]$DisplayName,
        [string]$Email,
        [string[]]$SecurityGroups,
        [boolean]$ForcePassChange = $False
    )

    $Endpoint = 'Services/SecurityService.ashx/SaveUser'

    $Security = Get-CWCSecurityConfigurationInfo -ErrorAction Stop
    $Internal = $Security.UserSources | Where-Object { $_.ResourceKey -eq $script:CwOption.InternalUserSource }
    $User = $Internal.Users | Where-Object { $_.Name -eq $UserToUpdate }
    if (!$User) { return Write-Error "Unable to find user $UserToUpdate" }

    $Update = @(
        $script:CwOption.InternalUserSource,
        $UserToUpdate,
        $User.Name,
        $null,
        $null,
        $User.PasswordQuestion,
        $User.DisplayName,
        '',
        $User.Email,
        $User.RoleNames,
        $False
    )

    if ($NewUserName) { $Update[2] = $NewUserName }
    if ($Password) {
        $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Password)
        $Update[3] = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr)
        $Update[4] = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr)
        Remove-Variable bstr
    }
    if ($OTP) { $Update[5] = $OTP }
    if ($DisplayName) { $Update[6] = $DisplayName }
    if ($Email) { $Update[8] = $Email }
    if ($SecurityGroups) { $Update[9] = $SecurityGroups }
    if ($ForcePassChange) { $Update[10] = $True }

    $Body = ConvertTo-Json $Update

    $WebRequestArguments = @{
        Endpoint = $Endpoint
        Body     = $Body
        Method   = 'Post'
    }
    if ($PSCmdlet.ShouldProcess($WebRequestArguments.Body, "Update-CWCUser")) {
        Invoke-CWCWebRequest -Arguments $WebRequestArguments
    }
}
function Get-CWCSessionGroup {
  [CmdletBinding()]
  param ()

  $Endpoint = 'Services/SessionGroupService.ashx/session-groups'

  $WebRequestArguments = @{
    Endpoint = $Endpoint
    Method   = 'Get'
  }

  Invoke-CWCWebRequest -Arguments $WebRequestArguments
}
$script:CwOption = @{
    InternalUserSource = 'InternalMembershipProvider'
    WebSession         = $null
}
function Invoke-CWCWebRequest {
    [CmdletBinding()]
    param(
        $Arguments,
        [int]$MaxRetry = 5
    )

    # Check that we have cached connection info
    if (!$script:CWCServerConnection) {
        $ErrorMessage = @()
        $ErrorMessage += 'Not connected to a Control server.'
        $ErrorMessage += '--> $CWCServerConnection variable not found.'
        $ErrorMessage += "----> Run 'Connect-CWC' to initialize the connection before issuing other CWC cmdlets."
        return Write-Error ($ErrorMessage | Out-String)
    }

    $BaseURI = "https://$($script:CWCServerConnection.Server)"
    $Arguments.URI = Join-Url $BaseURI $Arguments.Endpoint
    $Arguments.remove('Endpoint')
    $Arguments.UseBasicParsing = $true
    $Arguments.WebSession = $script:CWCServerConnection.WebSession
    Write-Debug "Arguments: $($Arguments | ConvertTo-Json)"

    # Issue request
    try { $Result = Invoke-WebRequest @Arguments }
    catch {
        # Start error message
        $ErrorMessage = @()

        if ($_.Exception.Response -and $PSVersionTable.PSVersion.Major -lt 6) {
            # Read exception response
            $ErrorStream = $_.Exception.Response.GetResponseStream()
            $Reader = New-Object System.IO.StreamReader($ErrorStream)
            $script:ErrBody = $Reader.ReadToEnd() | ConvertFrom-Json

            if ($ErrBody.code) {
                $ErrorMessage += 'An exception has been thrown.'
                $ErrorMessage += "--> $($ErrBody.code)"
                if ($ErrBody.code -eq 'Unauthorized') {
                    $ErrorMessage += "-----> $($ErrBody.message)"
                    $ErrorMessage += "-----> Use 'Disconnect-CWC' or 'Connect-CWC -Force' to set new authentication."
                }
                else {
                    $ErrorMessage += "-----> $($ErrBody.code): $($ErrBody.message)"
                    $ErrorMessage += '-----> ^ Error has not been documented please report. ^'
                }
            }
            elseif ($_.Exception.message) {
                $ErrorMessage += 'An exception has been thrown.'
                $ErrorMessage += "--> $($_.Exception.message)"
            }
        }

        if ($_.ErrorDetails) {
            $ErrorMessage += 'An error has been thrown.'
            $script:ErrDetails = $_.ErrorDetails
            $ErrorMessage += "--> $($ErrDetails.code)"
            $ErrorMessage += "--> $($ErrDetails.message)"
            if ($ErrDetails.errors.message) {
                $ErrorMessage += "-----> $($ErrDetails.errors.message)"
            }
        }

        if ($ErrorMessage.Length -lt 1) { $ErrorMessage = $_ }
        else { $ErrorMessage += $_.ScriptStackTrace }

        return Write-Error ($ErrorMessage | Out-String)
    }

    # Not sure this will be hit with current iwr error handling
    # May need to move to catch block need to find test
    # TODO Find test for retry
    # Retry the request
    $Retry = 0
    while ($Retry -lt $MaxRetry -and $Result.StatusCode -eq 500) {
        $Retry++
        # ConnectWise Manage recommended wait time
        $Wait = $([math]::pow( 2, $Retry))
        Write-Warning "Issue with request, status: $($Result.StatusCode) $($Result.StatusDescription)"
        Write-Warning "$($Retry)/$($MaxRetry) retries, waiting $($Wait)ms."
        Start-Sleep -Milliseconds $Wait
        $Result = Invoke-WebRequest @Arguments -UseBasicParsing
    }
    if ($Retry -ge $MaxRetry) {
        return Write-Error "Max retries hit. Status: $($Result.StatusCode) $($Result.StatusDescription)"
    }


    if ($Arguments.OutFile) {
        return $Result
    }
    return $Result.content | ConvertFrom-Json
}
function Join-Url {
    param (
        [parameter(Mandatory = $True)]
        [string] $Path,
        [parameter(Mandatory = $True)]
        [string] $ChildPath
    )
    "$($Path.TrimEnd('/'))/$($ChildPath.TrimStart('/'))"
}