Check-Sectigo.ps1


<#PSScriptInfo
 
.VERSION 1.0.5
 
.GUID d251a2b1-e289-4802-8442-06e3fb92ab7b
 
.AUTHOR Tomas Stanislawski
 
.COMPANYNAME H�gskolan Kristianstad
 
.COPYRIGHT 2021
 
.TAGS nagios sectigo certificate
 
.LICENSEURI
 
.PROJECTURI
 
.ICONURI
 
.EXTERNALMODULEDEPENDENCIES
 
.REQUIREDSCRIPTS
 
.EXTERNALSCRIPTDEPENDENCIES
 
.RELEASENOTES
First release
 
#>


<#
 
.DESCRIPTION
  Check expiry dates of certificates and Domin Control Verification (DCV) at Sectigo and report in Nagios friendly format
 Script has been tested in PowerShell Core on Linux.
 
 The script caches certificates in a local file, which is unfortunately needed because Sectigos API is so slow that with a certain amount of certificates the check takes longer than Nagios check timeout value (60.00 seconds)
 
 Run with: pwsh Check-Sectigo.ps1 -User username -Password password -Customer customer -Method method [-Domain domain]
  
 
#>
 
param($User,$Password,$Customer,$Domain,$Method,$Verbose)

# Static API Variables
$apiUri = "https://cert-manager.com:443/api"
$productTypes = "Digital Signature","Key Encipherment"

# Construct request header for all requests
[hashtable]$requestHead = @{
    'Content-Type' = "application/json"
    'login' = "$User"
    'password' = "$Password"
    'customerUri' = "$Customer"
}

# Other script variables
$progressPreference = 'silentlyContinue'
$allowedMethods = "IssuedCertificates","ValidationStatus"

# Variables for caching requests. NOTE: Cachefile must be created (touch xxx) and have access rights (chmod 666 xxx) before running first time!
$cacheFile = "/opt/plugins/custom/powershell/sectigo-cache.csv"
$maximumRunTimeSeconds = 30

if ($Verbose) {
    $VerbosePreference="Continue"
    }

######### NAGIOS SPECIFIC #########

# Variables, days to warning or critical
$warningTreshold = 35
$criticalTreshold = 20

# Exit codes
$returnOK = 0
$returnWarning = 1
$returnCritical = 2
$returnUnknown = 3

# Default plugin output object
$nagiosProperties = @{
    Textstatus = "UNKNOWN"
    Textoutput = "Should't reach this part"
    PerformanceData = ""
    Longtext = ""
    ReturnCode = "$returnUnknown"
}
$nagiosObject = New-Object psobject -Property $nagiosProperties

# Check if parameters are given, if not then break
if (!$User -or !$Password -or !$Customer -or !$Method)
{
    $nagiosObject.Textoutput = "Check parameters User, Password, Customer or Method missing"
}

# Specify TLS level since Digicert seems to require it
[Net.ServicePointManager]::SecurityProtocol = "tls12, tls11, tls"

<#
.Synopsis
   Short description
.DESCRIPTION
   Long description
.EXAMPLE
   Example of how to use this cmdlet
.EXAMPLE
   Another example of how to use this cmdlet
.INPUTS
   Inputs to this cmdlet (if any)
.OUTPUTS
   Output from this cmdlet (if any)
.NOTES
   General notes
.COMPONENT
   The component this cmdlet belongs to
.ROLE
   The role this cmdlet belongs to
.FUNCTIONALITY
   The functionality that best describes this cmdlet
#>

function Get-SectigoDCVStatus
{
    Param
    (
        # Domain
        [Parameter(Mandatory=$true)]
        $domainParam
    )
    Begin
    {
        # Default output properties, should all else fail
        $Properties = @{
            Textstatus = "UNKNOWN"
            Textoutput = "Unknown error in DCV for $domainParam"
            PerformanceData = ""
            Longtext = ""
            ReturnCode = "$returnUnknown"
        }

        # Set the DCV request body
        [hashtable]$requestBody = @{
            'domain' = $Domain
        }
        $now = Get-Date
    }
    Process
    {

        # Fetch results
        try {
            $reqResult = (Invoke-WebRequest -Method POST -Headers $requestHead -Uri "$apiUri/dcv/v2/validation/status" -Body ($requestBody | ConvertTo-Json) -UseBasicParsing) | ConvertFrom-Json
            }
        catch {
            $Properties.TextStatus = "WARNING"            
            $Properties.ReturnCode = $returnWarning
            if ($Error[0])
            {
                $Properties.Textoutput = $($Error[0] | ConvertFrom-Json).description
            }

            
        }

        # Parse result
        if ($reqResult)
        {
            # If status i Validated, set to OK, all others to WARNING
            switch ($reqResult.status)
            {
                'VALIDATED' { 
                        $Properties.TextStatus = "OK" 
                        $Properties.ReturnCode = $returnOK
                    }
                Default { 
                        $Properties.TextStatus = "WARNING"
                        $Properties.ReturnCode = $returnWarning
                    }
            }

            # Check how many days are left on the validation and set status accordingly
            $days_remaining = (New-TimeSpan -Start $now -End (Get-Date $reqResult.expirationDate)).days
            if ($days_remaining -lt $criticalTreshold) { 
                    $Properties.TextStatus = "CRITICAL"
                    $Properties.ReturnCode = $returnCritical
                }
                elseif (($days_remaining -lt $warningTreshold) -and ($days_remaining -gt $criticalTreshold)) { 
                    $Properties.TextStatus = "WARNING"
                    $Properties.ReturnCode = $returnWarning
                }
                elseif ($days_remaining -eq 999) { 
                    $Properties.TextStatus = "UNKNOWN"
                    $Properties.ReturnCode = $returnUnknown
                }
                elseif ($days_remaining -gt $warningTreshold) { 
                    $Properties.TextStatus = "OK"
                    $Properties.ReturnCode = $returnOK
                }
                else { 
                    $Properties.TextStatus = "UNKNOWN" 
                    $Properties.ReturnCode = $returnUnknown
                }
            $Properties.TextOutput = "$($reqResult.status), expires $($reqResult.expirationDate) (in $days_remaining days)"
            $Properties.Longtext = "Orderstatus: $($reqResult.orderStatus)"

        }
        
    }
    End
    {
        # Return the Properties object
        return (New-Object psobject -Property $Properties)
    }
}

<#
.Synopsis
   Short description
.DESCRIPTION
   Long description
.EXAMPLE
   Example of how to use this cmdlet
.EXAMPLE
   Another example of how to use this cmdlet
.INPUTS
   Inputs to this cmdlet (if any)
.OUTPUTS
   Output from this cmdlet (if any)
.NOTES
   General notes
.COMPONENT
   The component this cmdlet belongs to
.ROLE
   The role this cmdlet belongs to
.FUNCTIONALITY
   The functionality that best describes this cmdlet
#>

function Get-SectigoIssuedCertificates
{

    Begin
    {
        # Default output properties, should all else fail
        $Properties = @{
            Textstatus = "UNKNOWN"
            Textoutput = "Unknown error in issued certificates"
            PerformanceData = ""
            Longtext = ""
            ReturnCode = "$returnUnknown"
        }
    }
    Process
    {
        ### Fetch certificate ids from Sectigo
        $size = 50
        $position = 0

        try
        {
        $currentCertificateList = do
                           {
                                # Set the certifcate list request body
                                [hashtable]$requestBody = @{
                                    'size' = $size
                                    'status' = "Issued"
                                    'position' = "$position"
                                }

                                # Fetch results and increase position counter
                                $result = (Invoke-WebRequest -Headers $requestHead -Uri "$apiUri/ssl/v1" -Body $requestBody -UseBasicParsing).Content | ConvertFrom-Json
                                $result
                                $position = $position + $size
                        
                           }
                           until ($result.count -eq 0)
        }
        catch
        {
            $Properties.TextStatus = "WARNING"            
            if ($Error[0])
            {
                $Properties.Textoutput = $($Error[0] | ConvertFrom-Json).description
                $Properties.ReturnCode = $returnWarning
            }
        }

         # If Sectigo returned any certificates, do..
        if ($currentCertificateList)
        {

            $now = Get-Date

            ### Get cached certificate information
            $cachedCertificates = Get-Content -Path $cacheFile | ConvertFrom-Csv

            # From the cached certificates, pick out those in need of update.
            # These are certificates that: 1) are in the current Sectigo list
            # 2) have a last_checked value that exists and is older than 24 hours
            # 3) is missing a last_checked value

            $needToUpdate = $cachedCertificates | 
                                Where-Object {($PSItem.sslId -in $currentCertificateList.sslId) `
                                         -and ($PSItem.last_checked -and ((Get-Date $PSItem.last_checked) -lt $now.AddHours(-24)) `
                                         -or (!$PSItem.last_checked))}


            # Update needed certificates with full data from Sectigo
            $updatedCertificates = foreach ($sslId in $needToUpdate.sslId)
                                    {
                                        (Invoke-WebRequest -Headers $requestHead -Uri "$apiUri/ssl/v1/$sslId" -UseBasicParsing).Content | ConvertFrom-Json

                                        # Break loop if timeout exceeded
                                        if ((New-TimeSpan �Start $now �End (Get-Date)).TotalSeconds -gt $maximumRunTimeSeconds) 
                                        { 
                                            break 
                                        }
                                    }



            # Combine current, cached and updated certificates to one object
            $certificates = foreach ($certificate in $currentCertificateList)
                            {

                                Remove-Variable cachedCertificate,updatedCertificate -ErrorAction:SilentlyContinue
                                $cachedCertificate = $cachedCertificates | Where-Object {$PSItem.sslid -eq $certificate.sslId}
                                $updatedCertificate = $updatedCertificates | Where-Object {$PSItem.sslid -eq $certificate.sslId}

                                $sslId = $certificate.sslId
                                $status = if ($updatedCertificate) { $updatedCertificate.status } elseif ($cachedCertificate) { $cachedCertificate.status }
                                $common_name = $certificate.commonname
                                $valid_till = if ($updatedCertificate.expires) { Get-Date $updatedCertificate.expires } elseif ($cachedCertificate) { $cachedCertificate.valid_till }
                                $days_remaining = if ($valid_till) { (New-TimeSpan -Start $now -End $valid_till).days } else { 999 }
                                $last_checked = if ($updatedCertificate) { $now } elseif ($cachedCertificate) { $cachedCertificate.last_checked }
                                $nagios_status = if ($days_remaining -lt $criticalTreshold) { "CRITICAL" }
                                                    elseif (($days_remaining -lt $warningTreshold) -and ($days_remaining -gt $criticalTreshold)) { "WARNING" }
                                                    elseif ($days_remaining -eq 999) { "UNKNOWN" }
                                                    elseif ($days_remaining -gt $warningTreshold) { "OK" }
                                                    else { "UNKNOWN" }
                                $product_name_id = if ($updatedCertificate.status) { $updatedCertificate.certType.id } elseif ($cachedCertificate) { $cachedCertificate.product_name_id }

                                $certProperties = @{
                                    sslId = $sslId
                                    status = $status
                                    common_name = $common_name
                                    valid_till = $valid_till
                                    days_remaining = $days_remaining
                                    last_checked = $last_checked
                                    nagios_status = $nagios_status
                                    product_name_id = $product_name_id 
                                }
                            
                                New-Object psobject -Property $certProperties
                            }

            # Dump combined and updated object to cache file
            $certificates | Convertto-Csv | Out-File -FilePath $cacheFile

            $padNagiosStatus = ($certificates.nagios_status | Measure-Object -Maximum -Property Length).Maximum
            $padCommonName = ($certificates.common_name | Measure-Object -Maximum -Property Length).Maximum


            # Parse and build Nagios data
            # Sort and build the longtext by days_remaining
            foreach ($certificate in ($certificates | Sort-Object -Property days_remaining))
            {
                if ($certificate.product_name_id -eq "code_signing")
                {
                    $commonName = "Code signing"
                } else {
                    $commonName = $certificate.common_name
                }

                $Properties.Longtext = $Properties.Longtext + "$(($certificate.nagios_status).PadRight($padNagiosStatus," ")) - $($commonName.PadRight($padCommonName," ")) - $($certificate.days_remaining) days`n"
            }
            # Find the worst status in the list and set status accordingly
            if ($certificates.nagios_status -contains "CRITICAL")
            {
                $Properties.ReturnCode = $returnCritical
                $Properties.Textstatus = "CRITICAL"
                $Properties.Textoutput = "$(($certificates |
                                        Where-Object {$PSItem.nagios_status -eq "CRITICAL"}).count) in critical state, $(($certificates |
                                                        Where-Object {$PSItem.nagios_status -eq "CRITICAL"} |
                                        Sort-Object -Property days_remaining) |
                                        foreach { "$($PSItem.common_name) ($($PSItem.days_remaining)d)" } )"


            } elseif ($certificates.nagios_status -contains "WARNING")
            {
                $Properties.ReturnCode = $returnWarning
                $Properties.Textstatus = "WARNING"
                $Properties.Textoutput = "$(($certificates |
                                        Where-Object {$PSItem.nagios_status -eq "WARNING"}).count) in warning state, $(($certificates |
                                                        Where-Object {$PSItem.nagios_status -eq "WARNING"} |
                                        Sort-Object -Property days_remaining) |
                                        foreach { "$($PSItem.common_name) ($($PSItem.days_remaining)d)" } )"


            } elseif ($certificates.nagios_status -contains "OK")
            {
                $Properties.ReturnCode = $returnOK
                $Properties.Textstatus = "OK"
                $Properties.Textoutput = "All certificates within tresholds"


            } else
            {

                $Properties.Code = $returnUnknown
                $Properties.Textstatus = "UNKNOWN"
                $Properties.Textoutput = "Cache file probably missing, run script again or create it manually"

            }

            $Properties.PerformanceData = '''total_issued''=' + $($certificates.Count)

        } else {

            # This triggers only if no certificates were gotten
            $Properties.Textstatus = "WARNING"
            $Properties.Textoutput = "Got no certificates"
            $Properties.PerformanceData = ""
            $Properties.Longtext = ""
            $Properties.ReturnCode = "$returnWarning"

        }

    }
    End
    {
        return (New-Object psobject -Property $Properties)
    }
}

# What metod was chosen?
switch ($Method)
{
    'IssuedCertificates' {
            $nagiosObject = Get-SectigoIssuedCertificates
        }
    'ValidationStatus' {
            # if DCV chosen, check if there is a domain or not
            switch ($Domain)
            {
                {$PSItem} {
                        $nagiosObject = Get-SectigoDCVStatus -domainParam $Domain        
                    }
                Default {
                        $nagiosObject.Textoutput = "DCV status requested but '-Domain contoso.com' missing"
                    }
            }
            
        }
    {$PSItem -notin $allowedMethods} {
            $nagiosObject.Textoutput = "Unknown method. Allowed methods are $allowedMethods"
        }
    Default {
            $nagiosObject.Textoutput = "Is there life on mars?"

        }
}

# Spit out the results
$OutputEncoding = [ System.Text.Encoding]::UTF8
Write-Host "$($nagiosObject.Textstatus) - $($nagiosObject.Textoutput)|$($nagiosObject.PerformanceData)`n$($nagiosObject.Longtext)" -NoNewline

# And exit with a code
Exit $Properties.ReturnCode