Identifying endpoint license usage

There is a KB explaining how license usage works: https://support.sophos.com/support/s/article/KB-000035892?language=en_US

Server licenses sum the number of servers online in the last 30 days.

User/computer licenses sum:

  • the number of users with at least one device online in the last 30 days
  • the number of devices with no last user

Identifying which specific users and/or devices are contributing to current usage can be done manually or programmatically. 

Manual steps and a script to do it automatically are included below. 

Manual steps to determine user/computer license usage:

  1. Export the computers report (not users)
  2. Open in Excel or equivalent
  3. Sort by last connected time, and remove any offline >30 days
  4. Count those with no "last user".
  5. Deduplicate the "last user" field to leave only unique entries
  6. Count the number of deduplicated users (note: don't include a blank entry as it will have already been counted as part of step 4).
  7. Add up the totals from step 4 and 6 to give the current license usage. It should be [devices online in the last 30 days with no user] + [users with one or more devices online in the last 30 days]

A PowerShell script to do the same task using the Sophos Central public APIs is provided below:

By using or accessing the Software below, you agree to be bound by the terms of the Sophos End User License Agreement.

param ([switch] $ShowDetails)
<#
    Author.....: Sophos Sales Engineering
    Description: Script to calculate license usage from Sophos Central
	Version 1.0: Initial release
#>

$DeviceListA = @{}
$DeviceListU = @()
$ServerListU = @()


# Check if Central API Credentials have been stored, if not then prompt the user to add them
if ((Test-Path $env:userprofile\sophos_central_admin.json) -eq $false){
	# Prompt for Credentials
	$clientId = Read-Host "Please Enter your Client ID"
	$clientSecret = Read-Host "Please Enter your Client Secret" -AsSecureString | ConvertFrom-SecureString

	# Out to JSON Config File
	ConvertTo-Json $ClientID, $ClientSecret | Out-File $env:userprofile\sophos_central_admin.json -Force
}

# Read Credentials from JSON Config File
$credentials = Get-Content $env:userprofile\sophos_central_admin.json | ConvertFrom-Json
$clientId = $credentials[0]
$clientSecret = $credentials[1] | ConvertTo-SecureString


# Create PSCredential Object for Credentials
$SecureCredentials = New-Object System.Management.Automation.PSCredential -ArgumentList $clientId , $clientSecret


# SOPHOS OAuth URL
$TokenURI = "https://id.sophos.com/api/v2/oauth2/token"

# TokenRequestBody for oAuth2
$TokenRequestBody = @{
	"grant_type" = "client_credentials";
	"client_id" = $SecureCredentials.GetNetworkCredential().Username;
	"client_secret" = $SecureCredentials.GetNetworkCredential().Password;
	"scope" = "token";
}
$TokenRequestHeaders = @{
	"content-type" = "application/x-www-form-urlencoded";
}

# Set TLS Version
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

# Post Request to SOPHOS OAuth2 token:
$APIAuthResult = (Invoke-RestMethod -Method Post -Uri $TokenURI -Body $TokenRequestBody -Headers $TokenRequestHeaders -ErrorAction SilentlyContinue -ErrorVariable ScriptError)

# If there's an error requesting the token, say so, display the error, and break:
if ($ScriptError) {
	Write-Output "FAILED - Unable to retreive SOPHOS API Authentication Token - $($ScriptError)"
	Break
}

# Set the Token for use later on:
$script:Token = $APIAuthResult.access_token

# SOPHOS Whoami URI:
$WhoamiURI = "https://api.central.sophos.com/whoami/v1"

# SOPHOS Whoami Headers:
$WhoamiRequestHeaders = @{
	"Content-Type" = "application/json";
	"Authorization" = "Bearer $script:Token";
}

# Post Request to SOPHOS for Whoami Details:
$APIWhoamiResult = (Invoke-RestMethod -Method Get -Uri $WhoamiURI -Headers $WhoamiRequestHeaders -ErrorAction SilentlyContinue -ErrorVariable ScriptError)

# Set TenantID and ApiHost for use later on:
$script:ApiTenantId = $APIWhoamiResult.id
$script:ApiHost = $APIWhoamiResult.apiHosts.dataRegion	
	
# SOPHOS Endpoint API Headers:
$TentantAPIHeaders = @{
	"Authorization" = "Bearer $script:Token";
	"X-Tenant-ID" = "$script:ApiTenantId";
}
if ($apihost -ne $null){

	# Get List of Servers that were active in the last 30 days:
    do {
	    $ServersCounted = (Invoke-RestMethod -Method Get -Uri $script:ApiHost"/endpoint/v1/endpoints?pageTotal=true&pageFromKey=$NextKey&type=server&lastSeenAfter=-P30D&fields=hostname%2ClastSeenAt%2CassociatedPerson&sort=lastSeenAt" -Headers $TentantAPIHeaders -ErrorAction SilentlyContinue -ErrorVariable ScriptError)
        $script:NextKey = $ServersCounted.pages.nextKey

        foreach ($device in $ServersCounted.items) {
            $script:Hostname = $($device.hostname)
            $ServerListU += $Hostname
        }
    } while ($NextKey -ne $null)         
 
    # Calculate the number of servers that were active in the last 30 days
    $script:ServersCountedTotal = $ServerListU.Count
   
	# Get List of Devices that were active in the last 30 days:
    do {
        $DevicesCounted = (Invoke-RestMethod -Method Get -Uri $script:ApiHost"/endpoint/v1/endpoints?pageTotal=true&pageFromKey=$NextKey&type=computer&lastSeenAfter=-P30D&fields=hostname%2ClastSeenAt%2CassociatedPerson&sort=lastSeenAt" -Headers $TentantAPIHeaders -ErrorAction SilentlyContinue -ErrorVariable ScriptError)
        $script:NextKey = $DevicesCounted.pages.nextKey

        foreach ($device in $DevicesCounted.items) {
            $script:Hostname = $($device.hostname)

            if ($($device.associatedPerson.ID) -eq $null) {
                $DeviceListU += $Hostname
            } else {
                $script:UserInfo = @{}
                $script:UserInfo.Name = $($device.associatedPerson.name)
                $script:UserInfo.ID = $($device.associatedPerson.ID)
                $script:UserInfo.Login = $($device.associatedPerson.viaLogin)
                $DeviceListA.add($Hostname, $UserInfo)
            }
        }
    } while ($NextKey -ne $null) 

    # Calculate the number of unique users and endpoints without users assigned that were active in the last 30 days
    $script:DevicesCountedTotal = $DevicesCounted.pages.items
    $script:UsersCountedTotal = ($DeviceListA.Values.ID | select -Unique).Count
    $script:DevLicsCountedTotal = $DeviceListU.Count

	# Get List of Servers that have not been active during the last 30 days:
	$ServersIgnored = (Invoke-RestMethod -Method Get -Uri $script:ApiHost"/endpoint/v1/endpoints?pageTotal=true&type=server&lastSeenBefore=-P30D&fields=hostname%2ClastSeenAt%2CassociatedPerson&sort=lastSeenAt" -Headers $TentantAPIHeaders -ErrorAction SilentlyContinue -ErrorVariable ScriptError)
    $script:ServersIgnoredTotal = $ServersIgnored.pages.items

   	# Get List of Devices that have not been active during the last 30 days:
	$DevicesIgnored = (Invoke-RestMethod -Method Get -Uri $script:ApiHost"/endpoint/v1/endpoints?pageTotal=true&type=computer&lastSeenBefore=-P30D&fields=hostname%2ClastSeenAt%2CassociatedPerson&sort=lastSeenAt" -Headers $TentantAPIHeaders -ErrorAction SilentlyContinue -ErrorVariable ScriptError)
    $script:DevicesIgnoredTotal = $DevicesIgnored.pages.items

}

Write-Output "Sophos Central - License Usage"
Write-Output "=========================================================================================="
Write-Output "For the calculation of the license usage the following rules apply:"
Write-Output "* Server Protection is always licensed by device"
Write-Output "* Endpoint Protection is always licensed by user." 
Write-Output "- If the user is unknown then the device itself will consume a license"
Write-Output "* Devices that have been offline for more then 30 days do not consume licenses."
Write-Output ""
Write-Output "Summary:"
Write-Output "--------"
Write-Output "Number of servers consuming licenses............: $ServersCountedTotal"
Write-Output "Number of servers not using licenses............: $ServersIgnoredTotal"
Write-Output ""
Write-Output "Number of devices active in the last 30 days....: $DevicesCountedTotal"
Write-Output "Number of devices not active in the last 30 days: $DevicesIgnoredTotal"
Write-Output ""
Write-Output "Number of devices consuming licenses............: $DevLicsCountedTotal"
Write-Output "Number of users consuming licenses..............: $UsersCountedTotal"
Write-Output ""

if ($ShowDetails) {
    Write-Output ""
    Write-Output "Details:"
    Write-Output "--------"
    Write-Output "Servers consuming licenses:"
    foreach ($device in $ServerListU) {
        Write-Output "$device " 
    }
    Write-Output ""
    
    Write-Output "Devices without a user assigned consuming licenses:"
    foreach ($device in $DeviceListU) {
        Write-Output "$device "
    }
    Write-Output ""

    Write-Output "Devices assigned to users consuming licenses:"
    foreach ($key in $DeviceListA.keys) {
        $message = '{0} assigned to {1}' -f $key, $DeviceListA[$key].Name
        Write-Output "$message "
    }

}

If you call the script with the optional parameter -ShowDetails the script will output the computer names and the names of the user they are assigned to as well.



added showdetails parameter info at the end
[edited by: JS at 1:06 PM (GMT -8) on 28 Jan 2022]
  • Hello, thanks for this important information.

    I didn't get any entries for "devices online in the last 30 days with no user" because from the initial computer setup IT person will logged in. 

    I was able to get this "users with one or more devices online in the last 30 days", but this count doesnt match with the actual license count found in the licensing tab for some reason. 

  • Hi ,

    It is normal to not have any devices with no user assigned to them because as soon an someone logged on to them once the device will be assigned to that user, if someone else next logs on to the device then the device will be moved to that user. 

    If the license count does not match then this is most likely due to the fact that the value of "LastSeenAt" which is checked by the script is can lag for up to 7 days, so the number of licenses displayed in Sophos Central could be slightly higher (unless you have a lot of computers that have been offline for severall weeks).