HOWTO: Get Webroot Endpoints using Unity REST API and PowerShell

Webroot has recently released a new REST API that allows us as administrators to pull detailed endpoint data programmatically. What this effectively means is that all of the information that is presented to us in the Webroot Global Site Manager can now be extracted directly and integrated into other processes. I very much wanted a script that would be able be able to run on a scheduled task and compare all the systems in Active Directory with those registered in Webroot and then report on those AD systems that either do not have Webroot installed or haven’t checked in for more than a week.

It took some reading and trial and error but I managed to create a PowerShell script that can connect to Webroot and pull all of the details for every endpoint for a given keycode into an object which you can then do whatever you want with. I figured I’d save you the frustration of figuring out how to make this. Of course this code is presented as is. It’s working for me but your mileage may vary.

Here’s how it works:

1) You specify the Keycode you want to report on and provide your regular end user credentials as well as a special API client ID and password which you can create in the GSM
2) The script will then request a REST API token which is valid for 300 seconds
3) It will then use that token to request the siteID of the specified keycode
4) It will then use that siteid to grab all of the endpoints and their details for every endpoint associated with the given keycode and display the results

If you find this useful, let me know in the comments.

# The base URL for which all REST operations will be performed against
$BaseURL = 'https://unityapi.webrootcloudav.com'

# The keycode for the site that you wish to extract endpoint details from
$Keycode = 'AAAA-BBBB-CCCC-DDDD-EEEE'

# An administrator user for your Webroot portal -- this is typically the same user you use to login to the main portal
$WebrootUser = 'user@company.com'

# This is typically the same password used to log into the main portal
$WebrootPassword = 'mypassword'

# This must have previously been generated from the Webroot GSM for the site you wish to view
$APIClientID = 'client_abcdefgh@company.com'
$APIPassword = 'generatedpassword'

# You must first get a token which will be good for 300 seconds of future queries.  We do that from here
$TokenURL = "$BaseURL/auth/token"

# Once we have the token, we must get the SiteID of the site with the keycode we wish to view Endpoints from
$SiteIDURL = "$BaseURL/service/api/console/gsm/$KeyCode/sites"

# All Rest Credentials must be first converted to a base64 string so they can be transmitted in this format
$Credentials = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($APIUsername+":"+$APIPassword ))

write-host "Processing connection 1 of 3 (Obtain an access token)" -ForegroundColor Green
$Params = @{
            "ErrorAction" = "Stop"
            "URI" = $TokenURL
            "Headers" = @{"Authorization" = "Basic "+ $Credentials}
            "Body" = @{
                          "username" = $WebrootUser
                          "password" = $WebrootPassword
                          "grant_type" = 'password'
                          "scope" = '*'
                        }
            "Method" = 'post'
            "ContentType" = 'application/x-www-form-urlencoded'
            }

$AccessToken = (Invoke-RestMethod @Params).access_token

write-host "Processing connection 2 of 3 (Obtain the site ID for the provided keycode)" -ForegroundColor Green
$Params = @{
            "ErrorAction" = "Stop"
            "URI" = $SiteIDURL
            "ContentType" = "application/json"
            "Headers" = @{"Authorization" = "Bearer "+ $AccessToken}
            "Method" = "Get"
        }

$SiteID = (Invoke-RestMethod @Params).Sites.SiteId

write-host "Processing connection 3 of 3 (Get list of all endpoints and their details)" -ForegroundColor Green
$EndpointURL = "$BaseURL/service/api/console/gsm/$KeyCode/sites/$SiteID" +'/endpoints?PageSize=1000'

$Params = @{
            "ErrorAction" = "Stop"
            "URI" = $EndpointURL
            "ContentType" = "application/json"
            "Headers" = @{"Authorization" = "Bearer "+ $AccessToken}
            "Method" = "Get"
        }

$AllEndpoints = (Invoke-RestMethod @Params)

$AllEndpoints.Endpoints | Format-Table


9 comments

Skip to comment form

    • Josh on January 17, 2017 at 8:21 pm
    • Reply

    This is great work, thank you! I did find an error on line 24 – instead of $APIUsername it should read $APIClientID

    • Boris on September 29, 2017 at 2:52 pm
    • Reply

    Hello, thankyou for the script. It was a good basis for me. With multiple Sites, the code needs to be adjusted a bit (GSM Masterkeycode and Site Keycode)

    Rg. Boris

    • Tim Wiser on July 29, 2019 at 2:14 pm
    • Reply

    Hi,

    I’ve populated the script with the info it needed but just get the following when I run it:

    Invoke-RestMethod : {“statusCode”:400,”requestId”:”ecac366d-e5c7-49da-807a-0a55c8f3ffd8″,”error”:”invalid_client”,”error_description”:”client_id has not been sent.”,”AdditionalInformation”:{}}

    Does it handle 2FA being on the account that’s being used for accessing Unity?

    Tim

    1. It’s been a while since I set that up. But the code we use internally is still working. You have to enable the API in your Webroot tenant. We were one of the first customers to get it so we had it enabled manually by Webroot support. I don’t know if it’s exposed in the GUI or not now. Once you do that, you’ll generate a unique API username and password. This bypasses any kind of 2FA features that Webroot provides.

      My best advice would be to ensure you have an API username and password and if not, open a ticket with Webroot support to find out how to do so.

        • Tim Wiser on July 31, 2019 at 11:33 am
        • Reply

        Yeah, it’s a touch weird. I’ve tested connectivity using Postman so I know my credentials are correct, but feeding the same into the script and running it just yields the 400 (Bad Request) error message I posted above. I’m wondering whether something is being chewed up when copying and pasting the script somehow? Assuming you have my email address, are you able to put your script into a text file and send it over to me direct ….. please? 🙂

        1. Below I’ve copied and pasted the actual production code I’m using that I confirmed is working:

          # The display name of the company you wish to retrieve as multiple companies can exist in the same keycode that each otherwise return their own site id
          $CompanyName = ‘Company Name Goes Here’

          # Provide the API credentials previously created to connect to Webroot
          $APIClientID = ‘client_abc1234@company.com’
          $APIPassword = ‘complexpassword’

          # In addition to the API password, a user account credential is also required.
          $WebrootUser = ‘yourregularuseryoulogintowebrootwith@company.com’
          $WebRootPassword = ‘differentpassword’

          # We have to specify the Keycode that we want to pull data from
          $Keycode = ‘yourwebrootkeycode’

          # Specify the URL we will connect to initially to obtain a token that will be used for future connections
          $TokenURL = ‘https://unityapi.webrootcloudav.com/auth/token’

          # Once we have the token, this is where we will pull the desired endpoint data from
          $MainURL = “https://unityapi.webrootcloudav.com/service/api/console/gsm/$KeyCode/sites”

          # The REST API requires that the credentials are based as a base 64 string
          $Credentials = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($APIClientID+”:”+$APIPassword ))

          write-Progress “Processing connection 1 of 3 (Obtain an access token)”
          $Params = @{
          “ErrorAction” = “Stop”
          “URI” = $TokenURL
          “Headers” = @{“Authorization” = “Basic “+ $Credentials}
          “Body” = @{
          “username” = $WebrootUser
          “password” = $WebrootPassword
          “grant_type” = ‘password’
          “scope” = ‘*’
          }
          “Method” = ‘post’
          “ContentType” = ‘application/x-www-form-urlencoded’
          }

          $AccessToken = (Invoke-RestMethod @Params).access_token

          write-Progress “Processing connection 2 of 3 (Obtain the site ID for the provided keycode)”
          $Params = @{
          “ErrorAction” = “Stop”
          “URI” = $MainURL
          “ContentType” = “application/json”
          “Headers” = @{“Authorization” = “Bearer “+ $AccessToken}
          “Method” = “Get”
          }

          $SiteID = ((Invoke-RestMethod @Params).Sites | where {$_.sitename -eq $CompanyName}).siteid

          write-progress “Processing connection 3 of 3 (Get list of all endpoints and their details)”
          $EndpointURL = “https://unityapi.webrootcloudav.com/service/api/console/gsm/$KeyCode/sites/$SiteID” +’/endpoints?PageSize=1000′

          $Params = @{
          “ErrorAction” = “Stop”
          “URI” = $EndpointURL
          “ContentType” = “application/json”
          “Headers” = @{“Authorization” = “Bearer “+ $AccessToken}
          “Method” = “Get”
          }

          # Now that all of the connection details are set, connect to the Webroot console and get a master list of all active endpoints protected by Webroot
          $AllEndpoints = (Invoke-RestMethod @Params)

          $Allendpoints

    • Jon on August 30, 2019 at 10:49 pm
    • Reply

    Thank you for sharing! This worked for me

    • anon on October 11, 2019 at 5:41 pm
    • Reply

    Hi Robbie,

    Great article, and thank you for doing this for everyone’s benefit. I tried to do this from scratch, but the documentation is incredibly confusing for an API newb like me.

    A few quick questions.

    1. Any general API tips you can give a rookie like me? Tried reading up on APIs and whatnot and I get the basic premise, but each platform has vastly different documentation with respect to what headers are needed and etc. It’s all very confusing, any general tips that you can help me out with? Maybe a TLDR? I feel like once I get it, it will be much easier and I’ll be able to do this for any other platform going forward.

    2. The script appears to work, but whenever I try to connect I get this error, do you know how to get past this? Thanks in advance!

    The error content generated:

    Error 1:
    ========
    Invoke-RestMethod : {“statusCode”:400,”requestId”:”censored-GUID-goes-here”,”error”:null,”error_description”:”Error(GSMSITE009):Not a GSM Console”,”AdditionalInformation”:{“transname”:”GSMSITESAPI”,”languagecode”:”EN”}}
    At PATHLOCATION:\webroot.ps1:50 char:13
    + $SiteID = ((Invoke-RestMethod @Params).Sites | where {$_.sitename -eq …
    + ~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-RestMethod], WebException
    + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand

    Error 2:
    ========
    Invoke-RestMethod : {“statusCode”:400,”requestId”:”censored-GUID-goes-here”,”error”:null,”error_description”:”The specified id must contain 32 digits (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).”,”AdditionalInformation”:{}}
    At PATHLOCATION:\webroot.ps1:50 char:13
    + $AllEndpoints = (Invoke-RestMethod @Params)
    + ~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-RestMethod], WebException
    + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand

    Also, in case you are wondering, the GUID which I’ve censored for this post does indeed match the format the error is complaining about. I counted the characters and I am not sure why it is complaining about this. Any advice or do you know why it is doing this? Thank you in advance!

    • anon on October 11, 2019 at 5:46 pm
    • Reply

    Hi Robbie,

    Same person from last post; quick update.

    So in your code where $Keycode is defined, I had to change it to the site code as opposed to the product key which I is different. Once I did that I was able to resolve Error #1 in my last comment.

    I am still getting Error #2 though. Any thoughts? Thanks again.

Leave a Reply to Tim Wiser Cancel reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.