Sophos Central Licensing API

Disclaimer: This information is provided as-is for the benefit of the Community. Please contact Sophos Professional Services if you require assistance with your specific environment.

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


Introduction:
Only recently a new Sophos Central API was released which allows you to to access licensing details within Sophos Central. This obviously calls for a new PowerShell script, which can be used by partners or customers using the Enterprise Dashboard, that warns them when licenses are to expire within a specified number of days.

Steps:

The steps below guide you through process of preparing the script for your own use. 

  1. Create a Service Principal with the Service Principal ReadOnly role (other roles can be used as well but the ReadOnly role has the least privileges). How this is done is described in Gettings Started as a Partner respectively Getting Started as an Organization. If you already created a Service Principal then you can reuse it for this dashboard as well.

  2. Create a directory in which you store the Get-Central-Licenses.ps1 from the following code block
    param ([switch] $SaveCredentials, [switch] $Mail, [switch] $Export, [int] $Days = 90 )
    <#
    	Description: Retrieve overview of customers/tenants with licences that will expire soon
    	Parameters: -SaveCredentials -> will store then entered credentials locally on the PC, this is needed once
    				-Mail -> mail the results, make sure to specify sender and recipient details below
    				-Days -> include licenses expiring in n days (default is 90 days)
    				-Export -> Export the results to "Sophos-licenses-<datestamp>.csv" 
    #>
    
    # Email Settings, please note that we are using an Gmail account for sending the email.
    # For Gmail ensure that you generate an app password and activate MFA for the account! 
    $SmtpSrvr = "smtp.gmail.com"
    $SmtpPort = 587
    $SmtpUser = "myaccount@gmail.com"
    $SmtpPass = "mypassword"
    $SmtpRcpt = "recipient@domain.com"
    
    # Error Parser for Web Request
    function ParseWebError($WebError) {
    	if ($PSVersionTable.PSVersion.Major -lt 6) {
    		$resultStream = New-Object System.IO.StreamReader($WebError.Exception.Response.GetResponseStream())
    		$resultBody = $resultStream.readToEnd()  | ConvertFrom-Json
    	} else {
    		$resultBody = $WebError.ErrorDetails | ConvertFrom-Json
    	}
    
    	if ($null -ne $resultBody.message) {
    		return $resultBody.message
    	} else {
    		return $WebError.Exception.Response.StatusCode
    	}
    }
    
    # HTML blocks for the HTML based email
    $Header = @"
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> 
    <html xmlns="http://www.w3.org/1999/xhtml"> 
    <head> 
    <style>
    table {border-width: 1px; border-style: solid; border-color: #FFFFFF; border-collapse: collapse; }
    th {border-width: 1px; padding: 1px 5px 1px 5px; border-style: solid; border-color: #E0E0E0; background-color: #6495ED; color:#FFFFFF; text-align: left;}
    td {border-width: 1px; padding: 1px 5px 1px 5px; border-style: solid; border-color: #E0E0E0; text-align: left;}
    td.tenant {border-width: 1px; padding: 1px 5px 1px 5px; border-style: solid; border-color: #E0E0E0; background-color: #E0E0E0; text-align: left; }
    td.expired {border-width: 1px; padding: 1px 5px 1px 5px; border-style: solid; border-color: #E0E0E0; color:#FF0000; text-align: left;}
    td.expiring {border-width: 1px; padding: 1px 5px 1px 5px; border-style: solid; border-color: #E0E0E0; color:#FDA809; text-align: left;}
    
    </style> 
    </head>
    <body>
    Below you will find an overview of customers with licenses that recenly expired or will expire within the next <b>$($Days) days</b>.<br>
    The overview contains all licenses of said customers, which will help to identify those customers that already renewed their licenses.<br><br>
    <table><colgroup><col/><col/><col/><col/><col/><col/></colgroup> 
    <tr><th>LicenseID</th><th>StartDate</th><th>EndDate</th><th>ProductCode</th><th>Type</th><th>Days</th></tr>
    "@
    
    $Footer = @"
    </table>
    <br>When <b>enterprise</b> is listed in the <b>Type</b> column then the customer is using Master Licening.</body></html>
    "@
    
    # Setup datatable to license details
    $LicenseList = New-Object System.Data.Datatable
    [void]$LicenseList.Columns.Add("TenantName")
    [void]$LicenseList.Columns.Add("TenantID")
    [void]$LicenseList.Columns.Add("LicenseID")
    [void]$LicenseList.Columns.Add("StartDate")
    [void]$LicenseList.Columns.Add("EndDate")
    [void]$LicenseList.Columns.Add("ProductCode")
    [void]$LicenseList.Columns.Add("Type")
    [void]$LicenseList.Columns.Add("DaysRemaining", "System.Int32") 
    $LicenseView = New-Object System.Data.DataView($LicenseList)
    
    # Setup hashtable for tenants with expiring lisences
    $TenantList = @{}
    
    
    Clear-Host
    Write-Output "==============================================================================="
    Write-Output "Sophos API - Get details of expiring licenses"
    Write-Output "==============================================================================="
    
    # Define the filename and path for the credential file
    $CredentialFile = (Get-Item $PSCommandPath ).DirectoryName+"\"+(Get-Item $PSCommandPath ).BaseName+".json"
    
    # Check if Central API Credentials have been stored, if not then prompt the user to enter the credentials
    if (((Test-Path $CredentialFile) -eq $false) -or $SaveCredentials){
    	# Prompt for Credentials
    	$clientId = Read-Host "Please Enter your Client ID"
    	$clientSecret = Read-Host "Please Enter your Client Secret" -AsSecureString 
    } else { 
    	# Read Credentials from JSON File
    	$credentials = Get-Content $CredentialFile | ConvertFrom-Json
    	$clientId = $credentials[0]
    	$clientSecret = $credentials[1] | ConvertTo-SecureString
    }
    
    # We are making use of the PSCredentials object to store the API credentials
    # The Client Secret will be encrypted for the user excuting the script
    # When scheduling execution of the script remember to use the same user context
    
    $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 for OAuth2 token
    try {
    	$APIAuthResult = (Invoke-RestMethod -Method Post -Uri $TokenURI -Body $TokenRequestBody -Headers $TokenRequestHeaders -ErrorAction SilentlyContinue -ErrorVariable ScriptError)
    	if ($SaveCredentials) {
    		$clientSecret = $clientSecret | ConvertFrom-SecureString
    		ConvertTo-Json $ClientID, $ClientSecret | Out-File $CredentialFile -Force
    	}
    } catch {
    	# If there's an error requesting the token, say so, display the error, and break:
    	Write-Output "" 
    	Write-Output "AUTHENTICATION FAILED - Unable to retreive SOPHOS API Authentication Token"
    	Write-Output "Please verify the credentials used!" 
    	Write-Output "" 
    	Write-Output "If you are working with saved credentials then you can reset them by calling"
    	Write-Output "this script with the -SaveCredentials parameter"
    	Write-Output "" 
    	Read-Host -Prompt "Press ENTER to continue..."
    	Break
    }
    
    # Set the Token for use later on:
    $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 $Token";
    }
    
    # Post Request to SOPHOS for Whoami Details:
    $WhoamiResult = (Invoke-RestMethod -Method Get -Uri $WhoamiURI -Headers $WhoamiRequestHeaders -ErrorAction SilentlyContinue -ErrorVariable ScriptError)
    
    # Save Response details
    $WhoamiID = $WhoamiResult.id
    $WhoamiType = $WhoamiResult.idType	
    
    # Check if we are using partner/organization credentials
    if (-not (($WhoamiType -eq "partner") -or ($WhoamiType -eq "organization"))) {
    	Write-Output "Aborting script - idType does not match partner or organization!"
    	Break
    }
    
    # SOPHOS Partner/Organization API Headers:
    if ($WhoamiType -eq "partner") {
    	$GetTenantsHeaders = @{
    		"Authorization" = "Bearer $Token";
    		"X-Partner-ID" = "$WhoamiID";
    	}
    } else {
    	$GetTenantsHeaders = @{
    		"Authorization" = "Bearer $Token";
    		"X-Organization-ID" = "$WhoamiID";
    	}
    }
    
    # Get all Tenants
    Write-Host ("Checking:")
    $GetTenantsPage = 1
    do {
    
    	if ($WhoamiType -eq "partner") {
    		$GetTenants = (Invoke-RestMethod -Method Get -Uri "https://api.central.sophos.com/partner/v1/tenants?pageTotal=true&pageSize=100&page=$GetTenantsPage" -Headers $GetTenantsHeaders -ErrorAction SilentlyContinue -ErrorVariable ScriptError)
    	} else {
    		$GetTenants = (Invoke-RestMethod -Method Get -Uri "https://api.central.sophos.com/organization/v1/tenants?pageTotal=true&pageSize=100&page=$GetTenantsPage" -Headers $GetTenantsHeaders -ErrorAction SilentlyContinue -ErrorVariable ScriptError)
    	}
    
    	foreach ($Tenant in $GetTenants.items) {
    
    		# Codepage stuff to ensure that powershell displays those nasty Umlauts and other special characters correctly
    		$ShowAs = $Tenant.showAs
    		$ShowAs = [System.Text.Encoding]::GetEncoding(28591).GetBytes($ShowAs)
    		$ShowAs = [System.Text.Encoding]::UTF8.GetString($ShowAs)
    
    		Write-Output ("+- $($ShowAs)... $(" " * 75)".Substring(0,75)) 
    
    		$TenantID = $Tenant.id
    		$TenantDataRegion = $Tenant.apiHost
    
    		# SOPHOS Endpoint API Headers:
    		$TenantHeaders = @{
    			"Authorization" = "Bearer $Token";
    			"X-Tenant-ID" = "$TenantID";
    			"Content-Type" = "application/json";
    		}
    
    		# Get License Details
    		if (-not $null -eq $TenantDataRegion) {
    			try {
    
    				$Licenses = (Invoke-RestMethod -Method Get -Uri "https://api.central.sophos.com/licenses/v1/licenses" -Headers $TenantHeaders -ErrorAction SilentlyContinue -ErrorVariable ScriptError)
    				foreach ($License in $Licenses.licenses) {
    					if ($null -ne $License.EndDate) {
    						$EndDate = [datetime]::parseexact($License.EndDate, 'yyyy-MM-dd', $null)
    						$DaysRemaining = ($EndDate - ((Get-Date).Date)).Days
    						[void]$LicenseList.Rows.Add($ShowAs, $Tenant.id, $License.licenseIdentifier, $License.startDate, $License.EndDate, $License.product.code, $License.type, $DaysRemaining)
    						
    						if ($TenantList.ContainsKey($Tenant.id)) {
    							if($TenantList[$Tenant.id] -gt $DaysRemaining) {
    								$TenantList[$Tenant.id] = $DaysRemaining
    							}
    						} else {
    							$TenantList.add($Tenant.id, $DaysRemaining)
    						}
    					}
    				}
    			} catch {
    				# Something went wrong, get error details...
    				$WebError = ParseWebError($_)
    				Write-Output "   --> $($WebError)"
    			}
    		} else {
    			Write-Output ("   --> Account not activated")
    		}
    		Start-Sleep -Milliseconds 250 # Slow down processing to prevent hitting the API rate limit
    	}
    	$GetTenantsPage++
    
    } while ($GetTenantsPage -le $GetTenants.pages.total)
    
    Write-Output ""
    Write-Output "-------------------------------------------------------------------------------"
    
    $LicenseView.RowFilter = "DaysRemaining <= '$($Days)'"
    
    if ($LicenseView.Count -gt 0) {
    
    	if ($Export) {
    		# Delete old export present --> delete it
    		$FileName = (Get-Item $PSCommandPath ).DirectoryName + "\Sophos-Licenses-" + ((Get-Date).ToString("yyyyMMdd")) + ".csv"
    		if (Test-Path -Path $FileName) {
    			Remove-Item $FileName
    		}
    
    		# Export all relavant data
    		$TenantList = $TenantList.GetEnumerator() | Sort-Object -property:Value
    		foreach ($Tenant in $TenantList) {
    			if ($Tenant.Value -le $Days) {
    				$LicenseView.RowFilter = "TenantID = '$($Tenant.Key)'"
    				$LicenseView | Export-Csv $FileName -Encoding UTF8 -NoTypeInformation -Append
    			}
    		}
    
    		Write-Output ""
    		Write-Output "The results were save to the following file:"
    		Write-Output $FileName
    	} 
    	
    	if ($Mail) {
    		# Prepare Table Contents
    		$TableBody = ""
    		$TenantList = $TenantList.GetEnumerator() | Sort-Object -property:Value
    		foreach ($Tenant in $TenantList) {
    			if ($Tenant.Value -le $Days) {
    				$LicenseView.RowFilter = "TenantID = '$($Tenant.Key)'"
    				$LicenseView.Sort = "DaysRemaining ASC"
    				$TableBody += '<tr><td colspan="6" class="tenant"><b>' + $LicenseView[0].TenantName + '</b></td></tr>'
    				if($LicenseView.Count -ne 0) {
    					foreach ($Row in $LicenseView) {
    
    						# Change text color based on end date
    						if ($Row.DaysRemaining -lt 0) { 
    							$Class = "expired"
    						} elseif ($Row.DaysRemaining -le $Days) {
    							$Class = "expiring"
    						} else {
    							$Class = ""
    						}
    
    						$TableBody += '<tr>'
    						$TableBody += '<td class="' + $($Class) + '">' + $Row.LicenseID + '</td>'
    						$TableBody += '<td class="' + $($Class) + '">' + $Row.StartDate + '</td>'
    						$TableBody += '<td class="' + $($Class) + '">' + $Row.EndDate + '</td>'
    						$TableBody += '<td class="' + $($Class) + '">' + $Row.ProductCode + '</td>'
    						$TableBody += '<td class="' + $($Class) + '">' + $Row.Type + '</td>'
    						$TableBody += '<td class="' + $($Class) + '">' + $Row.DaysRemaining + '</td>'
    						$TableBody += '</tr>'
    					}
    				}
    				$TableBody += '<tr><td colspan="6"></td></tr>'
    			}
    		}
    
    		# Prepare Email
    		$Message = New-Object System.Net.Mail.MailMessage
    		$Message.From = $SmtpUser
    		$Message.To.Add($SmtpRcpt)
    		$Message.Subject = "Sophos Licenses expiring soon!"
    		$Message.IsBodyHtml = $true
    		$Message.Body = $Header + $TableBody + $Footer
    
    		# Create the SmtpClient object and send the Email
    		$Smtp = New-Object Net.Mail.SmtpClient($SmtpSrvr, $SmtpPort)
    		$Smtp.EnableSsl = $true
    		if ($SmtpPass -ne "") {
    			$Smtp.Credentials = New-Object System.Net.NetworkCredential( $SmtpUser , $SmtpPass );
    		}
    		$Smtp.Send($Message)
    
    		Write-Output ""
    		Write-Output "The results were sent by email to $($SmtpRcpt)"
    
    	} 
    	
    	if ((-Not $Mail) -And (-Not $Export)) {
    		Write-Output "Licenses that already expired or are expiring within the next $($Days) days:"    
    		$LicenseList.Select("DaysRemaining <= '$($Days)'","DaysRemaining ASC, TenantName ASC") | Select-Object TenantName, LicenseID, StartDate, EndDate, ProductCode, Type, DaysRemaining | Format-Table
    	}
    
    } else {
    	Write-Output ""
    	Write-Output "No licenses are expiring within the next $($Days) days!"    
    }
    
    Write-Output "==============================================================================="


     
  3. To ensure that we can run the script unattended, we are going to run our PowerShell script for the first time with the parameter -SaveCredentials. This parameter stores the credentials of our Service Principal in a JSON-file, we do this so that we do not have to enter these credentials every time we run the script, for example:

    C:\Sophos\Get-Central-Licenses.ps1 -SaveCredentials

    This will scan all tenants linked to your partner or organization account, during the scan it will display the tenant name being checked (if a tenant cannot be accessed then the script will display an error indicating why the data could not be accessed). Once the scan completes it will display a simple list of all licenses that are expiring next.

    From this point on you should call the PowerShell script without the -SaveCredentials parameter (unless your credentials are no longer valid, and you need to update them).

  4. Now that everything is prepared we have to decide in which form we want our output, the script offers two methods (besides the screen output you saw during step 4), namely by email and/or as an CSV-file. Both options contain all licenses for tenants where at least one license already expired or is expiring within the specified number of days (by default the script uses 90 days but you can override this with the -Days <number of days> parameter). 

    For an export call the script with the -Export parameter, this will store the results in a the CSV-file named Sophos-Licenses-<date stamp>.csv.

    If you want to use the mail-option of the script you will have to modify several parameters within the script itself before you can use the -Mail parameter, namely on line 12 till 16 where you will find the following variables: $SmtpSrvr, $SmtpPort, $SmtpUser, $SmtpPass and $SmtpRcptIf you have an internal mail server that allows connections on port 25 without authentication then you can leav$SmtpPass empty, if the mail server only accepts authenticated connections then you obviously have to specify $SmtpPass as well.

    Please note that when you are using Gmail account, like I do for my scripts, then you will have to prepare this account accordingly. To be able to sent emails you namely have to activate MFA within your Gmail account and once this has been done create an application password, for more details see: https://support.google.com/accounts/answer/185833?hl=en.  

    The Email that will be generated will make use of colors to make it easy to identify those licenses that already expired, are expiring soon and are still valid as can be seen in the screenshot at the top of this post.

  5. Now that we have verified that everything works the only thing left is to schedule the PowerShell script with the -Mail and/or -Export parameter on a regular basis. Under Windows you could for example use the Windows Task Scheduler.

I hope that you found the information in this recommended read post useful. If you did then please klick the like button in the upper right.

PS. The PowerShell script included in this post was used with PowerShell 5.1 on Windows and PowerShell 7.4 on Linux



Cosmetic changes
[edited by: Marcel at 8:03 AM (GMT -7) on 7 Aug 2024]
Parents
  • Disclaimer: This information is provided as-is for the benefit of the Community. Please contact Sophos Professional Services if you require assistance with your specific environment.

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


    Recently we added support for Sophos Firewall Subscriptions to the Sophos Central Licensing API, which obviously called for an additional script which utilizes this functionality..

    The script uses the exact same logic as the one posted above, the only difference being that it uses Sophos Firewall subscription details. It also uses the exact same parameters (-Days, -Mail, -Export and -SaveCredentials). Therefore, please follow the instructions in the post above to get started.

     

    param ([switch] $SaveCredentials, [switch] $Mail, [switch] $Export, [int] $Days = 90 )
    <#
    	Description: Retrieve overview of customers/tenants with licences that will expire soon
    	Parameters: -SaveCredentials -> will store then entered credentials locally on the PC, this is needed once
    				-Mail -> mail the results, make sure to specify sender and recipient details below
    				-Days -> include licenses expiring in n days (default is 90 days)
    				-Export -> Export the results to "Sophos-licenses-<datestamp>.csv" 
    #>
    
    # Email Settings, please note that we are using an Gmail account for sending the email.
    # For Gmail ensure that you generate an app password and activate MFA for the account! 
    $SmtpSrvr = "smtp.gmail.com"
    $SmtpPort = 587
    $SmtpUser = "myaccount@gmail.com"
    $SmtpPass = "mypassword"
    $SmtpRcpt = "recipient@domain.com"
    
    # Error Parser for Web Request
    function ParseWebError($WebError) {
    	if ($PSVersionTable.PSVersion.Major -lt 6) {
    		$resultStream = New-Object System.IO.StreamReader($WebError.Exception.Response.GetResponseStream())
    		$resultBody = $resultStream.readToEnd()  | ConvertFrom-Json
    	} else {
    		$resultBody = $WebError.ErrorDetails | ConvertFrom-Json
    	}
    
    	if ($null -ne $resultBody.message) {
    		return $resultBody.message
    	} else {
    		return $WebError.Exception.Response.StatusCode
    	}
    }
    
    # HTML blocks for the HTML based email
    $Header = @"
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> 
    <html xmlns="http://www.w3.org/1999/xhtml"> 
    <head> 
    <style>
    table {border-width: 1px; border-style: solid; border-color: #FFFFFF; border-collapse: collapse; }
    th {border-width: 1px; padding: 1px 5px 1px 5px; border-style: solid; border-color: #E0E0E0; background-color: #6495ED; color:#FFFFFF; text-align: left;}
    td {border-width: 1px; padding: 1px 5px 1px 5px; border-style: solid; border-color: #E0E0E0; text-align: left;}
    td.tenant {border-width: 1px; padding: 1px 5px 1px 5px; border-style: solid; border-color: #E0E0E0; background-color: #A0A0A0; color:#FFFFFF; text-align: left; }
    td.firewall {border-width: 1px; padding: 1px 5px 1px 5px; border-style: solid; border-color: #E0E0E0; background-color: #E0E0E0; text-align: left; }
    td.expired {border-width: 1px; padding: 1px 5px 1px 5px; border-style: solid; border-color: #E0E0E0; color:#FF0000; text-align: left;}
    td.expiring {border-width: 1px; padding: 1px 5px 1px 5px; border-style: solid; border-color: #E0E0E0; color:#FDA809; text-align: left;}
    
    </style> 
    </head>
    <body>
    Below you will find an overview of customers with firewall subscriptions that recenly (within the last 30 days) expired or will expire within the next <b>$($Days) days</b>.<br><br>
    <table><colgroup><col/><col/><col/><col/><col/><col/></colgroup> 
    <tr><th>LicenseID</th><th>StartDate</th><th>EndDate</th><th>ProductCode</th><th>Type</th><th>Days</th></tr>
    "@
    
    $Footer = @"
    </table>
    <br>When it states <b>FW claimed by: Unmanaged Account</b> then you will find the unique Sophos Central ID below that entry. 
    The unique Sophos Central ID can be used by Sophos to identify the account that was used to claim the firewall.<br><br>
    </body></html>
    "@
    
    # Setup datatable to license details
    $LicenseList = New-Object System.Data.Datatable
    [void]$LicenseList.Columns.Add("Source")
    [void]$LicenseList.Columns.Add("TenantID")
    [void]$LicenseList.Columns.Add("ClaimedBy")
    [void]$LicenseList.Columns.Add("SerialNumber")
    [void]$LicenseList.Columns.Add("LicenseID")
    [void]$LicenseList.Columns.Add("StartDate")
    [void]$LicenseList.Columns.Add("EndDate")
    [void]$LicenseList.Columns.Add("ProductCode")
    [void]$LicenseList.Columns.Add("Type")
    [void]$LicenseList.Columns.Add("DaysRemaining", "System.Int32")
    [void]$LicenseList.Columns.Add("IncludeInReport")
    $LicenseView = New-Object System.Data.DataView($LicenseList)
    
    Clear-Host
    Write-Output "==============================================================================="
    Write-Output "Sophos API - Get details of firewalls with expiring subscriptions"
    Write-Output "==============================================================================="
    
    # Define the filename and path for the credential file
    $CredentialFile = (Get-Item $PSCommandPath ).DirectoryName+"\"+(Get-Item $PSCommandPath ).BaseName+".json"
    
    # Check if Central API Credentials have been stored, if not then prompt the user to enter the credentials
    if (((Test-Path $CredentialFile) -eq $false) -or $SaveCredentials){
    	# Prompt for Credentials
    	$ClientId = Read-Host "Please Enter your Client ID"
    	if ($ClientId -eq "") {Break}
    	$clientSecret = Read-Host "Please Enter your Client Secret" -AsSecureString 
    } else { 
    	# Read Credentials from JSON File
    	$Credentials = Get-Content $CredentialFile | ConvertFrom-Json
    	$ClientId = $Credentials[0]
    	$ClientSecret = $Credentials[1] | ConvertTo-SecureString
    }
    
    # We are making use of the PSCredentials object to store the API credentials
    # The Client Secret will be encrypted for the user excuting the script
    # When scheduling execution of the script remember to use the same user context
    
    $SecureCredentials = New-Object System.Management.Automation.PSCredential -ArgumentList $ClientId , $ClientSecret
    
    # SOPHOS OAuth URL
    $AuthURI = "https://id.sophos.com/api/v2/oauth2/token"
    
    # Body and Header for oAuth2 Authentication
    $AuthBody = @{}
    $AuthBody.Add("grant_type", "client_credentials")
    $AuthBody.Add("client_id", $SecureCredentials.GetNetworkCredential().Username)
    $AuthBody.Add("client_secret", $SecureCredentials.GetNetworkCredential().Password)
    $AuthBody.Add("scope", "token")
    $AuthHead = @{}
    $AuthHead.Add("content-type", "application/x-www-form-urlencoded")
    
    # Set TLS Version
    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
    
    # Post Request to SOPHOS for OAuth2 token
    try {
        $Result = (Invoke-RestMethod -Method Post -Uri $AuthURI -Body $AuthBody -Headers $AuthHead -ErrorAction SilentlyContinue -ErrorVariable ScriptError)
        if ($SaveCredentials) {
    	    $ClientSecret = $ClientSecret | ConvertFrom-SecureString
    	    ConvertTo-Json $ClientID, $ClientSecret | Out-File $CredentialFile -Force
        }
    } catch {
        # If there's an error requesting the token, say so, display the error, and break:
        Write-Output "" 
    	Write-Output "AUTHENTICATION FAILED - Unable to retreive SOPHOS API Authentication Token"
        Write-Output "Please verify the credentials used!" 
        Write-Output "" 
        Write-Output "If you are working with saved credentials then you can reset them by calling"
        Write-Output "this script with the -SaveCredentials parameter"
        Write-Output "" 
        Read-Host -Prompt "Press ENTER to continue..."
        Break
    }
    
    # Set the Token for use later on:
    $Token = $Result.access_token
    
    # SOPHOS Whoami URI:
    $WhoamiURI = "https://api.central.sophos.com/whoami/v1"
    
    # SOPHOS Whoami Headers:
    $WhoamiHead = @{}
    $WhoamiHead.Add("Content-Type", "application/json")
    $WhoamiHead.Add("Authorization", "Bearer $Token")
    
    # Post Request to SOPHOS for Whoami Details:
    $Result = (Invoke-RestMethod -Method Get -Uri $WhoamiURI -Headers $WhoamiHead -ErrorAction SilentlyContinue -ErrorVariable ScriptError)
    
    # Save Response details
    $WhoamiID = $Result.id
    $WhoamiType = $Result.idType	
    
    # Check if we are using partner/organization credentials
    if (-not (($WhoamiType -eq "partner") -or ($WhoamiType -eq "organization"))) {
    	Write-Output "Aborting script - idType does not match partner or organization!"
    	Break
    }
    
    # SOPHOS Partner/Organization API Headers:
    if ($WhoamiType -eq "partner") {
    	$APIHeaders = @{}
    	$APIHeaders.Add("Authorization", "Bearer $Token")
    	$APIHeaders.Add("X-Partner-ID", "$WhoamiID")
    } else {
    	$APIHeaders = @{}
    	$APIHeaders.Add("Authorization", "Bearer $Token")
    	$APIHeaders.Add("X-Organization-ID", "$WhoamiID")
    }
    
    # Get Firewall Licensing details using the Partner Account
    if ($WhoamiType -eq "partner") {
    	Write-Output "[Licensing] Get firewall license details from the partner inventory"
    	$CurrentPage = 1
    	do {
    		try {
    			$Firewalls = (Invoke-RestMethod -Method Get -Uri "https://api.central.sophos.com/licenses/v1/licenses/firewalls?pageTotal=true&pageSize=500&Page=$CurrentPage" -Headers $APIHeaders -ErrorAction SilentlyContinue -ErrorVariable ScriptError)
    			foreach ($Firewall in $Firewalls.items) {
    				foreach ($License in $Firewall.licenses) {
    					if (-not ("usage" -eq $License.type -or "perpetual" -eq $License.type)) {
    						if (-not ($null -eq $License.EndDate)) {
    							$EndDate = [datetime]::parseexact("$($License.EndDate)", 'yyyy-MM-dd', $null)
    							$DaysRemaining = ($EndDate - ((Get-Date).Date)).Days
    							if ($DaysRemaining -ge -30) {
    								[void]$LicenseList.Rows.Add("Partner", $Firewall.tenant.id, "", $Firewall.serialNumber, $License.licenseIdentifier, $License.startDate, "$($License.EndDate)", $License.product.code, $License.type, $DaysRemaining, $False)
    							}
    						}
    					}
    				}
    			}
    		} catch {
    			# Something went wrong, get error details...
    			$WebError = ParseWebError($_)
    			Write-Output "   --> $($WebError)"
    		}
    		$CurrentPage++
    	} while ($CurrentPage -le $Firewalls.pages.total)
    }
    
    # Get all Tenants
    Write-Output "[Licensing] Get firewall license details from the tenants..."
    $TenantsPage = 1
    do {
    
    	if ($WhoamiType -eq "partner") {
    		$TenantList = (Invoke-RestMethod -Method Get -Uri "https://api.central.sophos.com/partner/v1/tenants?pageTotal=true&pageSize=100&page=$TenantsPage" -Headers $APIHeaders -ErrorAction SilentlyContinue -ErrorVariable ScriptError)
    	} else {
    		$TenantList = (Invoke-RestMethod -Method Get -Uri "https://api.central.sophos.com/organization/v1/tenants?pageTotal=true&pageSize=100&page=$TenantsPage" -Headers $APIHeaders -ErrorAction SilentlyContinue -ErrorVariable ScriptError)
    	}
    	foreach ($Tenant in $TenantList.items) {
    
    		# Codepage stuff to ensure that powershell displays those nasty Umlauts and other special characters correctly
    		$ShowAs = $Tenant.showAs
    		$ShowAs = [System.Text.Encoding]::GetEncoding(28591).GetBytes($ShowAs)
    		$ShowAs = [System.Text.Encoding]::UTF8.GetString($ShowAs)
    		Write-Output ("+- $($ShowAs)... $(" " * 75)".Substring(0,75)) 
    
    		# Update LicenseList --> replace TenantID with TenantName in the ClaimedBy column 
    		$LicenseView.RowFilter = "TenantID = '$($Tenant.id)'"
    		foreach ($License in $LicenseView) {
    			$License.ClaimedBy = $ShowAs
    		}
    
    		$TenantID = $Tenant.id
            $DataRegion = $Tenant.apiHost
    
    		# SOPHOS Tenant API Headers:
    		$TenantHead = @{}
    		$TenantHead.Add("Authorization", "Bearer $Token")
    		$TenantHead.Add("X-Tenant-ID", "$TenantID")
    		$TenantHead.Add("Content-Type", "application/json")
    		
    		# Get License Details from Tenants
    		if (-not $null -eq $DataRegion) {
    
    			# Get Firewall Licensing details using the Tenant Account
    			$CurrentPage = 1
    			do {
    				try {
    					$Firewalls = (Invoke-RestMethod -Method Get -Uri "https://api.central.sophos.com/licenses/v1/licenses/firewalls?pageTotal=true&pageSize=500&Page=$CurrentPage" -Headers $TenantHead -ErrorAction SilentlyContinue -ErrorVariable ScriptError)
    					foreach ($Firewall in $Firewalls.items) {
    						foreach ($License in $Firewall.licenses) {
    	
    							# Skip License if already know from the Partner Inventory
    							$LicenseView.RowFilter = "LicenseID = '$($License.licenseIdentifier)'"
    							if ($LicenseView.count -eq 0) {
    								if (-not ("usage" -eq $License.type -or "perpetual" -eq $License.type)) {
    									$EndDate = [datetime]::parseexact($License.EndDate, 'yyyy-MM-dd', $null)
    									$DaysRemaining = ($EndDate - ((Get-Date).Date)).Days
    									if ($DaysRemaining -ge -30) {
    										[void]$LicenseList.Rows.Add("Tenant", $TenantID, $ShowAs, $Firewall.serialNumber, $License.licenseIdentifier, $License.startDate, $License.EndDate, $License.product.code, $License.type, $DaysRemaining, $False)
    									}
    								}
    							}
    						}
    					}
    				} catch {
    					# Something went wrong, get error details...
    					$WebError = ParseWebError($_)
    					Write-Output "   --> $($WebError)"
    				}
    				$CurrentPage++
    			} while ($CurrentPage -le $Firewalls.pages.total)
    		} else {
    			Write-Output ("   --> Account not activated")
    		}
    		Start-Sleep -Milliseconds 250  # Slow down processing to prevent hitting the API rate limit
    	}
    	$TenantsPage++
    
    } while ($TenantsPage -le $TenantList.pages.total)
    
    Write-Output ""
    Write-Output "[Licensing] Populating claimed by details..."
    $LicenseView.RowFilter = "TenantID IS NULL"
    foreach ($License in $LicenseView) { $License.ClaimedBy = "Partner" }
    
    $LicenseView.RowFilter = "ClaimedBy = ''"
    foreach ($License in $LicenseView) { $License.ClaimedBy = "Unmanaged Account" }
    
    # Set IncludeInReport to TRUE for all records belonging to a Firewall containing an expiring license
    $LicenseView.RowFilter = "DaysRemaining <= '$($Days)'"
    $LicenseView.Sort = "DaysRemaining DESC"
    foreach ($License in $LicenseView) {
    	$Rows = $LicenseList.Select("SerialNumber = '$($License.SerialNumber)'")
    	foreach ($Row in $Rows) { $Row["IncludeInReport"] = $True }
    }
    
    Write-Output "[Licensing] Data collection completed."
    Write-Output "-------------------------------------------------------------------------------"
    
    $LicenseView.RowFilter = "IncludeInReport = 'TRUE'"
    $LicenseView.Sort = "ClaimedBy ASC, TenantID ASC, SerialNumber ASC, DaysRemaining ASC"
    
    if ($LicenseView.Count -gt 0) {
    
    	if ($Export) {
    		# Delete old export present --> delete it
    		$FileName = (Get-Item $PSCommandPath ).DirectoryName + "\Sophos-Subscriptions-" + ((Get-Date).ToString("yyyyMMdd")) + ".csv"
    		if (Test-Path -Path $FileName) {
    			Remove-Item $FileName
    		}
    
    		# Export all relavant data
    		$LicenseView | Export-Csv $FileName -Encoding UTF8 -NoTypeInformation -Append
    
    		Write-Output "The results were saved to the following file:"
    		Write-Output $FileName
    	} 
    	
    	if ($Mail) {
    		# Prepare Table Contents
    		$TableBody = ""
    		$TenantName = "<TenantName>"
    		$Firewall = "<Firewall>"
    
    		foreach ($Row in $LicenseView) {
    
    			if (($Row.ClaimedBy + $Row.TenantID) -ne $TenantName) {
    				$ShowCentralID = ''
    				if ($Row.ClaimedBy -eq "Unmanaged Account") {
    					$ShowCentralID = '<br><small>' + $Row.TenantID +'</small>'
    				} 
    				$TableBody += '<tr><td colspan="6" class="tenant"><b>FW claimed by: ' + $Row.ClaimedBy + $ShowCentralID +'</b></td></tr>'
    				$TenantName = $Row.ClaimedBy + $Row.TenantID
    			}
    
    			if ($Row.SerialNumber -ne $Firewall) {
    				$TableBody += '<tr><td colspan="6" class="firewall"><b>FW serial number: ' + $Row.SerialNumber + '</b></td></tr>'
    				$Firewall = $Row.SerialNumber
    			}
    
    			# Change text color based on end date
    			if ($Row.DaysRemaining -lt 0) { 
    				$Class = "expired"
    			} elseif ($Row.DaysRemaining -le $Days) {
    				$Class = "expiring"
    			} else {
    				$Class = ""
    			}
    
    			$TableBody += '<tr>'
    			$TableBody += '<td class="' + $($Class) + '">' + $Row.LicenseID + '</td>'
    			$TableBody += '<td class="' + $($Class) + '">' + $Row.StartDate + '</td>'
    			$TableBody += '<td class="' + $($Class) + '">' + $Row.EndDate + '</td>'
    			$TableBody += '<td class="' + $($Class) + '">' + $Row.ProductCode + '</td>'
    			$TableBody += '<td class="' + $($Class) + '">' + $Row.Type + '</td>'
    			$TableBody += '<td class="' + $($Class) + '">' + $Row.DaysRemaining + '</td>'
    			$TableBody += '</tr>'
    		}
    
    		# Prepare Email
    		$Message = New-Object System.Net.Mail.MailMessage
    		$Message.From = $SmtpUser
    		$Message.To.Add($SmtpRcpt)
    		$Message.Subject = "Sophos Firewall Subscriptions expiring soon!"
    		$Message.IsBodyHtml = $true
    		$Message.Body = $Header + $TableBody + $Footer
    
    		# Create the SmtpClient object and send the Email
    		$Smtp = New-Object Net.Mail.SmtpClient($SmtpSrvr, $SmtpPort)
    		$Smtp.EnableSsl = $true
    		if ($SmtpPass -ne "") {
    			$Smtp.Credentials = New-Object System.Net.NetworkCredential( $SmtpUser , $SmtpPass );
    		}
    		$Smtp.Send($Message)
    		Write-Output "The results were sent by email to $($SmtpRcpt)"
    
    	} 
    	
    	if ((-Not $Mail) -And (-Not $Export)) {
    		Write-Output "Licenses that recently expired or are expiring within the next $($Days) days:"    
    		$LicenseView | Select-Object Source, ClaimedBy, SerialNumber, LicenseID, StartDate, EndDate, ProductCode, Type, DaysRemaining | Format-Table
    	}
    
    } else {
    	Write-Output "No licenses are expiring within the next $($Days) days!"    
    }
    
    Write-Output "==============================================================================="

    I hope that you find this addition useful. If you did, then please klick the like button in the upper right.

Reply
  • Disclaimer: This information is provided as-is for the benefit of the Community. Please contact Sophos Professional Services if you require assistance with your specific environment.

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


    Recently we added support for Sophos Firewall Subscriptions to the Sophos Central Licensing API, which obviously called for an additional script which utilizes this functionality..

    The script uses the exact same logic as the one posted above, the only difference being that it uses Sophos Firewall subscription details. It also uses the exact same parameters (-Days, -Mail, -Export and -SaveCredentials). Therefore, please follow the instructions in the post above to get started.

     

    param ([switch] $SaveCredentials, [switch] $Mail, [switch] $Export, [int] $Days = 90 )
    <#
    	Description: Retrieve overview of customers/tenants with licences that will expire soon
    	Parameters: -SaveCredentials -> will store then entered credentials locally on the PC, this is needed once
    				-Mail -> mail the results, make sure to specify sender and recipient details below
    				-Days -> include licenses expiring in n days (default is 90 days)
    				-Export -> Export the results to "Sophos-licenses-<datestamp>.csv" 
    #>
    
    # Email Settings, please note that we are using an Gmail account for sending the email.
    # For Gmail ensure that you generate an app password and activate MFA for the account! 
    $SmtpSrvr = "smtp.gmail.com"
    $SmtpPort = 587
    $SmtpUser = "myaccount@gmail.com"
    $SmtpPass = "mypassword"
    $SmtpRcpt = "recipient@domain.com"
    
    # Error Parser for Web Request
    function ParseWebError($WebError) {
    	if ($PSVersionTable.PSVersion.Major -lt 6) {
    		$resultStream = New-Object System.IO.StreamReader($WebError.Exception.Response.GetResponseStream())
    		$resultBody = $resultStream.readToEnd()  | ConvertFrom-Json
    	} else {
    		$resultBody = $WebError.ErrorDetails | ConvertFrom-Json
    	}
    
    	if ($null -ne $resultBody.message) {
    		return $resultBody.message
    	} else {
    		return $WebError.Exception.Response.StatusCode
    	}
    }
    
    # HTML blocks for the HTML based email
    $Header = @"
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> 
    <html xmlns="http://www.w3.org/1999/xhtml"> 
    <head> 
    <style>
    table {border-width: 1px; border-style: solid; border-color: #FFFFFF; border-collapse: collapse; }
    th {border-width: 1px; padding: 1px 5px 1px 5px; border-style: solid; border-color: #E0E0E0; background-color: #6495ED; color:#FFFFFF; text-align: left;}
    td {border-width: 1px; padding: 1px 5px 1px 5px; border-style: solid; border-color: #E0E0E0; text-align: left;}
    td.tenant {border-width: 1px; padding: 1px 5px 1px 5px; border-style: solid; border-color: #E0E0E0; background-color: #A0A0A0; color:#FFFFFF; text-align: left; }
    td.firewall {border-width: 1px; padding: 1px 5px 1px 5px; border-style: solid; border-color: #E0E0E0; background-color: #E0E0E0; text-align: left; }
    td.expired {border-width: 1px; padding: 1px 5px 1px 5px; border-style: solid; border-color: #E0E0E0; color:#FF0000; text-align: left;}
    td.expiring {border-width: 1px; padding: 1px 5px 1px 5px; border-style: solid; border-color: #E0E0E0; color:#FDA809; text-align: left;}
    
    </style> 
    </head>
    <body>
    Below you will find an overview of customers with firewall subscriptions that recenly (within the last 30 days) expired or will expire within the next <b>$($Days) days</b>.<br><br>
    <table><colgroup><col/><col/><col/><col/><col/><col/></colgroup> 
    <tr><th>LicenseID</th><th>StartDate</th><th>EndDate</th><th>ProductCode</th><th>Type</th><th>Days</th></tr>
    "@
    
    $Footer = @"
    </table>
    <br>When it states <b>FW claimed by: Unmanaged Account</b> then you will find the unique Sophos Central ID below that entry. 
    The unique Sophos Central ID can be used by Sophos to identify the account that was used to claim the firewall.<br><br>
    </body></html>
    "@
    
    # Setup datatable to license details
    $LicenseList = New-Object System.Data.Datatable
    [void]$LicenseList.Columns.Add("Source")
    [void]$LicenseList.Columns.Add("TenantID")
    [void]$LicenseList.Columns.Add("ClaimedBy")
    [void]$LicenseList.Columns.Add("SerialNumber")
    [void]$LicenseList.Columns.Add("LicenseID")
    [void]$LicenseList.Columns.Add("StartDate")
    [void]$LicenseList.Columns.Add("EndDate")
    [void]$LicenseList.Columns.Add("ProductCode")
    [void]$LicenseList.Columns.Add("Type")
    [void]$LicenseList.Columns.Add("DaysRemaining", "System.Int32")
    [void]$LicenseList.Columns.Add("IncludeInReport")
    $LicenseView = New-Object System.Data.DataView($LicenseList)
    
    Clear-Host
    Write-Output "==============================================================================="
    Write-Output "Sophos API - Get details of firewalls with expiring subscriptions"
    Write-Output "==============================================================================="
    
    # Define the filename and path for the credential file
    $CredentialFile = (Get-Item $PSCommandPath ).DirectoryName+"\"+(Get-Item $PSCommandPath ).BaseName+".json"
    
    # Check if Central API Credentials have been stored, if not then prompt the user to enter the credentials
    if (((Test-Path $CredentialFile) -eq $false) -or $SaveCredentials){
    	# Prompt for Credentials
    	$ClientId = Read-Host "Please Enter your Client ID"
    	if ($ClientId -eq "") {Break}
    	$clientSecret = Read-Host "Please Enter your Client Secret" -AsSecureString 
    } else { 
    	# Read Credentials from JSON File
    	$Credentials = Get-Content $CredentialFile | ConvertFrom-Json
    	$ClientId = $Credentials[0]
    	$ClientSecret = $Credentials[1] | ConvertTo-SecureString
    }
    
    # We are making use of the PSCredentials object to store the API credentials
    # The Client Secret will be encrypted for the user excuting the script
    # When scheduling execution of the script remember to use the same user context
    
    $SecureCredentials = New-Object System.Management.Automation.PSCredential -ArgumentList $ClientId , $ClientSecret
    
    # SOPHOS OAuth URL
    $AuthURI = "https://id.sophos.com/api/v2/oauth2/token"
    
    # Body and Header for oAuth2 Authentication
    $AuthBody = @{}
    $AuthBody.Add("grant_type", "client_credentials")
    $AuthBody.Add("client_id", $SecureCredentials.GetNetworkCredential().Username)
    $AuthBody.Add("client_secret", $SecureCredentials.GetNetworkCredential().Password)
    $AuthBody.Add("scope", "token")
    $AuthHead = @{}
    $AuthHead.Add("content-type", "application/x-www-form-urlencoded")
    
    # Set TLS Version
    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
    
    # Post Request to SOPHOS for OAuth2 token
    try {
        $Result = (Invoke-RestMethod -Method Post -Uri $AuthURI -Body $AuthBody -Headers $AuthHead -ErrorAction SilentlyContinue -ErrorVariable ScriptError)
        if ($SaveCredentials) {
    	    $ClientSecret = $ClientSecret | ConvertFrom-SecureString
    	    ConvertTo-Json $ClientID, $ClientSecret | Out-File $CredentialFile -Force
        }
    } catch {
        # If there's an error requesting the token, say so, display the error, and break:
        Write-Output "" 
    	Write-Output "AUTHENTICATION FAILED - Unable to retreive SOPHOS API Authentication Token"
        Write-Output "Please verify the credentials used!" 
        Write-Output "" 
        Write-Output "If you are working with saved credentials then you can reset them by calling"
        Write-Output "this script with the -SaveCredentials parameter"
        Write-Output "" 
        Read-Host -Prompt "Press ENTER to continue..."
        Break
    }
    
    # Set the Token for use later on:
    $Token = $Result.access_token
    
    # SOPHOS Whoami URI:
    $WhoamiURI = "https://api.central.sophos.com/whoami/v1"
    
    # SOPHOS Whoami Headers:
    $WhoamiHead = @{}
    $WhoamiHead.Add("Content-Type", "application/json")
    $WhoamiHead.Add("Authorization", "Bearer $Token")
    
    # Post Request to SOPHOS for Whoami Details:
    $Result = (Invoke-RestMethod -Method Get -Uri $WhoamiURI -Headers $WhoamiHead -ErrorAction SilentlyContinue -ErrorVariable ScriptError)
    
    # Save Response details
    $WhoamiID = $Result.id
    $WhoamiType = $Result.idType	
    
    # Check if we are using partner/organization credentials
    if (-not (($WhoamiType -eq "partner") -or ($WhoamiType -eq "organization"))) {
    	Write-Output "Aborting script - idType does not match partner or organization!"
    	Break
    }
    
    # SOPHOS Partner/Organization API Headers:
    if ($WhoamiType -eq "partner") {
    	$APIHeaders = @{}
    	$APIHeaders.Add("Authorization", "Bearer $Token")
    	$APIHeaders.Add("X-Partner-ID", "$WhoamiID")
    } else {
    	$APIHeaders = @{}
    	$APIHeaders.Add("Authorization", "Bearer $Token")
    	$APIHeaders.Add("X-Organization-ID", "$WhoamiID")
    }
    
    # Get Firewall Licensing details using the Partner Account
    if ($WhoamiType -eq "partner") {
    	Write-Output "[Licensing] Get firewall license details from the partner inventory"
    	$CurrentPage = 1
    	do {
    		try {
    			$Firewalls = (Invoke-RestMethod -Method Get -Uri "https://api.central.sophos.com/licenses/v1/licenses/firewalls?pageTotal=true&pageSize=500&Page=$CurrentPage" -Headers $APIHeaders -ErrorAction SilentlyContinue -ErrorVariable ScriptError)
    			foreach ($Firewall in $Firewalls.items) {
    				foreach ($License in $Firewall.licenses) {
    					if (-not ("usage" -eq $License.type -or "perpetual" -eq $License.type)) {
    						if (-not ($null -eq $License.EndDate)) {
    							$EndDate = [datetime]::parseexact("$($License.EndDate)", 'yyyy-MM-dd', $null)
    							$DaysRemaining = ($EndDate - ((Get-Date).Date)).Days
    							if ($DaysRemaining -ge -30) {
    								[void]$LicenseList.Rows.Add("Partner", $Firewall.tenant.id, "", $Firewall.serialNumber, $License.licenseIdentifier, $License.startDate, "$($License.EndDate)", $License.product.code, $License.type, $DaysRemaining, $False)
    							}
    						}
    					}
    				}
    			}
    		} catch {
    			# Something went wrong, get error details...
    			$WebError = ParseWebError($_)
    			Write-Output "   --> $($WebError)"
    		}
    		$CurrentPage++
    	} while ($CurrentPage -le $Firewalls.pages.total)
    }
    
    # Get all Tenants
    Write-Output "[Licensing] Get firewall license details from the tenants..."
    $TenantsPage = 1
    do {
    
    	if ($WhoamiType -eq "partner") {
    		$TenantList = (Invoke-RestMethod -Method Get -Uri "https://api.central.sophos.com/partner/v1/tenants?pageTotal=true&pageSize=100&page=$TenantsPage" -Headers $APIHeaders -ErrorAction SilentlyContinue -ErrorVariable ScriptError)
    	} else {
    		$TenantList = (Invoke-RestMethod -Method Get -Uri "https://api.central.sophos.com/organization/v1/tenants?pageTotal=true&pageSize=100&page=$TenantsPage" -Headers $APIHeaders -ErrorAction SilentlyContinue -ErrorVariable ScriptError)
    	}
    	foreach ($Tenant in $TenantList.items) {
    
    		# Codepage stuff to ensure that powershell displays those nasty Umlauts and other special characters correctly
    		$ShowAs = $Tenant.showAs
    		$ShowAs = [System.Text.Encoding]::GetEncoding(28591).GetBytes($ShowAs)
    		$ShowAs = [System.Text.Encoding]::UTF8.GetString($ShowAs)
    		Write-Output ("+- $($ShowAs)... $(" " * 75)".Substring(0,75)) 
    
    		# Update LicenseList --> replace TenantID with TenantName in the ClaimedBy column 
    		$LicenseView.RowFilter = "TenantID = '$($Tenant.id)'"
    		foreach ($License in $LicenseView) {
    			$License.ClaimedBy = $ShowAs
    		}
    
    		$TenantID = $Tenant.id
            $DataRegion = $Tenant.apiHost
    
    		# SOPHOS Tenant API Headers:
    		$TenantHead = @{}
    		$TenantHead.Add("Authorization", "Bearer $Token")
    		$TenantHead.Add("X-Tenant-ID", "$TenantID")
    		$TenantHead.Add("Content-Type", "application/json")
    		
    		# Get License Details from Tenants
    		if (-not $null -eq $DataRegion) {
    
    			# Get Firewall Licensing details using the Tenant Account
    			$CurrentPage = 1
    			do {
    				try {
    					$Firewalls = (Invoke-RestMethod -Method Get -Uri "https://api.central.sophos.com/licenses/v1/licenses/firewalls?pageTotal=true&pageSize=500&Page=$CurrentPage" -Headers $TenantHead -ErrorAction SilentlyContinue -ErrorVariable ScriptError)
    					foreach ($Firewall in $Firewalls.items) {
    						foreach ($License in $Firewall.licenses) {
    	
    							# Skip License if already know from the Partner Inventory
    							$LicenseView.RowFilter = "LicenseID = '$($License.licenseIdentifier)'"
    							if ($LicenseView.count -eq 0) {
    								if (-not ("usage" -eq $License.type -or "perpetual" -eq $License.type)) {
    									$EndDate = [datetime]::parseexact($License.EndDate, 'yyyy-MM-dd', $null)
    									$DaysRemaining = ($EndDate - ((Get-Date).Date)).Days
    									if ($DaysRemaining -ge -30) {
    										[void]$LicenseList.Rows.Add("Tenant", $TenantID, $ShowAs, $Firewall.serialNumber, $License.licenseIdentifier, $License.startDate, $License.EndDate, $License.product.code, $License.type, $DaysRemaining, $False)
    									}
    								}
    							}
    						}
    					}
    				} catch {
    					# Something went wrong, get error details...
    					$WebError = ParseWebError($_)
    					Write-Output "   --> $($WebError)"
    				}
    				$CurrentPage++
    			} while ($CurrentPage -le $Firewalls.pages.total)
    		} else {
    			Write-Output ("   --> Account not activated")
    		}
    		Start-Sleep -Milliseconds 250  # Slow down processing to prevent hitting the API rate limit
    	}
    	$TenantsPage++
    
    } while ($TenantsPage -le $TenantList.pages.total)
    
    Write-Output ""
    Write-Output "[Licensing] Populating claimed by details..."
    $LicenseView.RowFilter = "TenantID IS NULL"
    foreach ($License in $LicenseView) { $License.ClaimedBy = "Partner" }
    
    $LicenseView.RowFilter = "ClaimedBy = ''"
    foreach ($License in $LicenseView) { $License.ClaimedBy = "Unmanaged Account" }
    
    # Set IncludeInReport to TRUE for all records belonging to a Firewall containing an expiring license
    $LicenseView.RowFilter = "DaysRemaining <= '$($Days)'"
    $LicenseView.Sort = "DaysRemaining DESC"
    foreach ($License in $LicenseView) {
    	$Rows = $LicenseList.Select("SerialNumber = '$($License.SerialNumber)'")
    	foreach ($Row in $Rows) { $Row["IncludeInReport"] = $True }
    }
    
    Write-Output "[Licensing] Data collection completed."
    Write-Output "-------------------------------------------------------------------------------"
    
    $LicenseView.RowFilter = "IncludeInReport = 'TRUE'"
    $LicenseView.Sort = "ClaimedBy ASC, TenantID ASC, SerialNumber ASC, DaysRemaining ASC"
    
    if ($LicenseView.Count -gt 0) {
    
    	if ($Export) {
    		# Delete old export present --> delete it
    		$FileName = (Get-Item $PSCommandPath ).DirectoryName + "\Sophos-Subscriptions-" + ((Get-Date).ToString("yyyyMMdd")) + ".csv"
    		if (Test-Path -Path $FileName) {
    			Remove-Item $FileName
    		}
    
    		# Export all relavant data
    		$LicenseView | Export-Csv $FileName -Encoding UTF8 -NoTypeInformation -Append
    
    		Write-Output "The results were saved to the following file:"
    		Write-Output $FileName
    	} 
    	
    	if ($Mail) {
    		# Prepare Table Contents
    		$TableBody = ""
    		$TenantName = "<TenantName>"
    		$Firewall = "<Firewall>"
    
    		foreach ($Row in $LicenseView) {
    
    			if (($Row.ClaimedBy + $Row.TenantID) -ne $TenantName) {
    				$ShowCentralID = ''
    				if ($Row.ClaimedBy -eq "Unmanaged Account") {
    					$ShowCentralID = '<br><small>' + $Row.TenantID +'</small>'
    				} 
    				$TableBody += '<tr><td colspan="6" class="tenant"><b>FW claimed by: ' + $Row.ClaimedBy + $ShowCentralID +'</b></td></tr>'
    				$TenantName = $Row.ClaimedBy + $Row.TenantID
    			}
    
    			if ($Row.SerialNumber -ne $Firewall) {
    				$TableBody += '<tr><td colspan="6" class="firewall"><b>FW serial number: ' + $Row.SerialNumber + '</b></td></tr>'
    				$Firewall = $Row.SerialNumber
    			}
    
    			# Change text color based on end date
    			if ($Row.DaysRemaining -lt 0) { 
    				$Class = "expired"
    			} elseif ($Row.DaysRemaining -le $Days) {
    				$Class = "expiring"
    			} else {
    				$Class = ""
    			}
    
    			$TableBody += '<tr>'
    			$TableBody += '<td class="' + $($Class) + '">' + $Row.LicenseID + '</td>'
    			$TableBody += '<td class="' + $($Class) + '">' + $Row.StartDate + '</td>'
    			$TableBody += '<td class="' + $($Class) + '">' + $Row.EndDate + '</td>'
    			$TableBody += '<td class="' + $($Class) + '">' + $Row.ProductCode + '</td>'
    			$TableBody += '<td class="' + $($Class) + '">' + $Row.Type + '</td>'
    			$TableBody += '<td class="' + $($Class) + '">' + $Row.DaysRemaining + '</td>'
    			$TableBody += '</tr>'
    		}
    
    		# Prepare Email
    		$Message = New-Object System.Net.Mail.MailMessage
    		$Message.From = $SmtpUser
    		$Message.To.Add($SmtpRcpt)
    		$Message.Subject = "Sophos Firewall Subscriptions expiring soon!"
    		$Message.IsBodyHtml = $true
    		$Message.Body = $Header + $TableBody + $Footer
    
    		# Create the SmtpClient object and send the Email
    		$Smtp = New-Object Net.Mail.SmtpClient($SmtpSrvr, $SmtpPort)
    		$Smtp.EnableSsl = $true
    		if ($SmtpPass -ne "") {
    			$Smtp.Credentials = New-Object System.Net.NetworkCredential( $SmtpUser , $SmtpPass );
    		}
    		$Smtp.Send($Message)
    		Write-Output "The results were sent by email to $($SmtpRcpt)"
    
    	} 
    	
    	if ((-Not $Mail) -And (-Not $Export)) {
    		Write-Output "Licenses that recently expired or are expiring within the next $($Days) days:"    
    		$LicenseView | Select-Object Source, ClaimedBy, SerialNumber, LicenseID, StartDate, EndDate, ProductCode, Type, DaysRemaining | Format-Table
    	}
    
    } else {
    	Write-Output "No licenses are expiring within the next $($Days) days!"    
    }
    
    Write-Output "==============================================================================="

    I hope that you find this addition useful. If you did, then please klick the like button in the upper right.

Children
No Data