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('/'))" } |