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 |