diff --git a/Get-ExpiringCertificateReport.ps1 b/Get-ExpiringCertificateReport.ps1 index cfeeece..f65e2cf 100644 --- a/Get-ExpiringCertificateReport.ps1 +++ b/Get-ExpiringCertificateReport.ps1 @@ -1,73 +1,78 @@ -<# - .SYNOPSIS - Generate a report of exiring certificates from an Active Directory Certificate Services Certificate Authority. - - .DESCRIPTION - This script checks ADCS Certificate Authorities for issued certificate requests that are expiring in the next 45 days. - Specify a list of template names to include, and it will translate that to their OIDs, find expiring certs using those - templates, and then send a report as directed. - - .PARAMETER Recipients - To-Do: Add a function parameter to send an email to specific recipients. - - .PARAMETER Output - To-Do: Add a function parameter to choose email, HTML, CSV, JSON, or console output. - - .INPUTS - None. You cannot pipe objects to this script. - - .OUTPUTS - Email - HTML, CSV, JSON, or XML file - Console - - .NOTES - Author: Sam Erde - Modified: 2023/07/21 - - Depends on the PSPKI module at https://www.powershellgallery.com/packages/PSPKI and the AD Certificate Services RSAT feature. - - To Do: Add checks for prerequisites, turn into function(s), take parameters for recipients and report output type, - get CAs in all domains in AD forest, error handling, show all template names (and optionally use Out-GridView/Out- - ConsoleGridView to select desired templates), use OGV to generate a text file containing templates and then use - that file as list of monitored certificate templates for expiring certificates report. -#> -$Version = "2023.07.21" -$Header = @" - -███╗ ██╗ ██████╗ ██████╗███████╗██████╗ ████████╗ -████╗ ██║██╔═══██╗ ██╔════╝██╔════╝██╔══██╗╚══██╔══╝ -██╔██╗ ██║██║ ██║ ██║ █████╗ ██████╔╝ ██║ -██║╚██╗██║██║ ██║ ██║ ██╔══╝ ██╔══██╗ ██║ -██║ ╚████║╚██████╔╝ ╚██████╗███████╗██║ ██║ ██║ -╚═╝ ╚═══╝ ╚═════╝ ╚═════╝╚══════╝╚═╝ ╚═╝ ╚═╝ - -██╗ ███████╗███████╗████████╗ ██████╗ ███████╗██╗ ██╗██╗███╗ ██╗██████╗ +function Get-ExpiringCertificateReport { + <# + .SYNOPSIS + Generate a report of exiring certificates from an Active Directory Certificate Services Certificate Authority. + + .DESCRIPTION + This script checks ADCS Certificate Authorities for issued certificate requests that are expiring in the next 45 days. + Specify a list of template names to include, and it will translate that to their OIDs, find expiring certs using those + templates, and then send a report as directed. + + .PARAMETER Recipients + To-Do: Add a function parameter to send an email to specific recipients. + + .PARAMETER Output + To-Do: Add a function parameter to choose email, HTML, CSV, JSON, or console output. + + .INPUTS + None. You cannot pipe objects to this script. + + .OUTPUTS + Email + HTML, CSV, JSON, or XML file + Console + + .NOTES + Author: Sam Erde + Modified: 2023/07/21 + + Depends on the PSPKI module at https://www.powershellgallery.com/packages/PSPKI and the AD Certificate Services RSAT feature. + + To Do: Add checks for prerequisites, turn into function(s), take parameters for recipients and report output type, + get CAs in all domains in AD forest, error handling, show all template names (and optionally use Out-GridView/Out- + ConsoleGridView to select desired templates), use OGV to generate a text file containing templates and then use + that file as list of monitored certificate templates for expiring certificates report. + #> + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost')] + param () + + $Version = '2023.07.21' + $Header = @" + +███╗ ██╗ ██████╗ ██████╗███████╗██████╗ ████████╗ +████╗ ██║██╔═══██╗ ██╔════╝██╔════╝██╔══██╗╚══██╔══╝ +██╔██╗ ██║██║ ██║ ██║ █████╗ ██████╔╝ ██║ +██║╚██╗██║██║ ██║ ██║ ██╔══╝ ██╔══██╗ ██║ +██║ ╚████║╚██████╔╝ ╚██████╗███████╗██║ ██║ ██║ +╚═╝ ╚═══╝ ╚═════╝ ╚═════╝╚══════╝╚═╝ ╚═╝ ╚═╝ + +██╗ ███████╗███████╗████████╗ ██████╗ ███████╗██╗ ██╗██╗███╗ ██╗██████╗ ██║ ██╔════╝██╔════╝╚══██╔══╝ ██╔══██╗██╔════╝██║ ██║██║████╗ ██║██╔══██╗ ██║ █████╗ █████╗ ██║ ██████╔╝█████╗ ███████║██║██╔██╗ ██║██║ ██║ ██║ ██╔══╝ ██╔══╝ ██║ ██╔══██╗██╔══╝ ██╔══██║██║██║╚██╗██║██║ ██║ ███████╗███████╗██║ ██║ ██████╔╝███████╗██║ ██║██║██║ ╚████║██████╔╝ -╚══════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚═════╝ +╚══════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚═════╝ + +v$Version -v$Version - "@ -Write-Host -ForegroundColor Cyan -BackgroundColor Black $Header + Write-Host -ForegroundColor Cyan -BackgroundColor Black $Header -# ════════════════════════════════════════════════════════╗ -# Modify these variables to suit your environment: ║ -# To, From, SMTPServer, DaysLeft, TemplateNamesIncluded ║ + # ════════════════════════════════════════════════════════╗ + # Modify these variables to suit your environment: ║ + # To, From, SMTPServer, DaysLeft, TemplateNamesIncluded ║ -$To = @('recipient1@example.com','recipient2@example.com') -$From = 'NoCertLeftBehind@example.com' -$SMTPServer = 'smtp.example.com' + $To = @('recipient1@example.com', 'recipient2@example.com') + $From = 'NoCertLeftBehind@example.com' + $SMTPServer = 'smtp.example.com' -# Change "DaysLeft" to whatever lead time you want for the notification of expiring certificates. -$DaysLeft = 30 + # Change "DaysLeft" to whatever lead time you want for the notification of expiring certificates. + $DaysLeft = 30 -# List the display names of the certificate templates that you want to monitor using a multi-line here-string that is converted to an array. -# This is simply easier than typing every name in quotes and separating them with a comma. -$TemplateNamesIncluded = (@" + # List the display names of the certificate templates that you want to monitor using a multi-line here-string that is converted to an array. + # This is simply easier than typing every name in quotes and separating them with a comma. + $TemplateNamesIncluded = (@' Root Certification Authority CA Exchange CEP Encryption @@ -91,137 +96,132 @@ Signature with Key Encipherment Subordinate Certification Authority Web Server WSUS Signing Certificate -"@).Split([Environment]::NewLine) - -# End of Customizations ║ -# ══════════════════════╝ - -# Import required modules and Windows features -if (Get-Module -Name 'PSPKI' -ListAvailable) { - Write-Information 'The PSPKI module is installed.' -} else { - Write-Information 'The PSPKI module is not installed. Attempting installation...' - try { - Install-Module -Name PSPKI -AllowClobber -Scope CurrentUser -Force - } - catch { - Write-Error 'PSPKI module installation failed.' - } -} -if ( (Get-WindowsCapability -Online -Name 'Rsat.CertificateServices.Tools~~~~0.0.1.0').Stated -eq 'Installed') { - Write-Information 'The Certificate Services RSAT feature is installed.' -} -else { - try { - Get-WindowsCapability -Online -Name 'Rsat.CertificateServices.Tools~~~~0.0.1.0' | Add-WindowsCapability -Online +'@).Split([Environment]::NewLine) + + # End of Customizations ║ + # ══════════════════════╝ + + # Import required modules and Windows features + if (Get-Module -Name 'PSPKI' -ListAvailable) { + Write-Information 'The PSPKI module is installed.' + } else { + Write-Information 'The PSPKI module is not installed. Attempting installation...' + try { + Install-Module -Name PSPKI -AllowClobber -Scope CurrentUser -Force + } catch { + Write-Error 'PSPKI module installation failed.' + } } - catch { - Write-Error 'Failed to install the Certificate Services RSAT feature. Please do so manually.' + if ( (Get-WindowsCapability -Online -Name 'Rsat.CertificateServices.Tools~~~~0.0.1.0').Stated -eq 'Installed') { + Write-Information 'The Certificate Services RSAT feature is installed.' + } else { + try { + Get-WindowsCapability -Online -Name 'Rsat.CertificateServices.Tools~~~~0.0.1.0' | Add-WindowsCapability -Online + } catch { + Write-Error 'Failed to install the Certificate Services RSAT feature. Please do so manually.' + } } -} -# End of module installation check + # End of module installation check -# ═══════════════════════════════════════════════════════════════════════════════════════╗ -# Shortcut (code snippet) variables: these make the following code simpler to work with: ║ + # ═══════════════════════════════════════════════════════════════════════════════════════╗ + # Shortcut (code snippet) variables: these make the following code simpler to work with: ║ -# Extract just the certificate authority's hostname from its configuration name. Used within Get-CertificateRequests below. -$CaName = @{ Name="CA"; Expression = {$_.ConfigString.Split("\")[1]} } + # Extract just the certificate authority's hostname from its configuration name. Used within Get-CertificateRequests below. + $CaName = @{ Name = 'CA'; Expression = { $_.ConfigString.Split('\')[1] } } -# Certificate age filter statement. Used within Get-CertificateRequests below. -$CertAgeFilter = "NotAfter -ge $(Get-Date)", "NotAfter -le $((Get-Date).AddDays($DaysLeft))" + # Certificate age filter statement. Used within Get-CertificateRequests below. + $CertAgeFilter = "NotAfter -ge $(Get-Date)", "NotAfter -le $((Get-Date).AddDays($DaysLeft))" -# Translate a certificate template OID to its readable display name. Used within Get-CertificateRequests below. -$CertTemplateName = @{ Name="TemplateName"; Expression = { - if ($_.CertificateTemplate -like "1*") { - (Get-CertificateTemplate -OID $_.CertificateTemplate).DisplayName - } - else { - $_.CertificateTemplate + # Translate a certificate template OID to its readable display name. Used within Get-CertificateRequests below. + $CertTemplateName = @{ Name = 'TemplateName'; Expression = { + if ($_.CertificateTemplate -like '1*') { + (Get-CertificateTemplate -OID $_.CertificateTemplate).DisplayName + } else { + $_.CertificateTemplate + } } - } -} # End CertTemplateName - -$Domain = $([System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain().Name) + } # End CertTemplateName -# End of shortcut (code snippet) variables ║ -# ═════════════════════════════════════════╝ + $Domain = $([System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain().Name) + # End of shortcut (code snippet) variables ║ + # ═════════════════════════════════════════╝ -# ══════════════════╗ -# Collect the data: ║ -# Find certificate authorities in the domain and get their hostname[s] -Write-Information "Finding certificate authorities in $Domain..." -$CANames = (Get-CA | Select-Object Computername).Computername -if ($CANames.Count -lt 1) { - Write-Warning "No certificate authorities were found in Active Directory." - Break -} -Write-Information "Found: $CANames `n" + # ══════════════════╗ + # Collect the data: ║ -# Get OIDs of all certificate templates that you want to monitor. The script takes MUCH longer when querying by template name. -Write-Information "Getting OIDs for $($TemplateNamesIncluded.Count) certificate templates..." -$TemplateOidsIncluded = foreach ($item in $TemplateNamesIncluded) { - try { - (Get-CertificateTemplate -DisplayName $item).Oid.Value - } - catch { - Write-Warning "Unable to get the certificate templates. Please review the error and try again." - Write-Error $error + # Find certificate authorities in the domain and get their hostname[s] + Write-Information "Finding certificate authorities in $Domain..." + $CANames = (Get-CA | Select-Object Computername).Computername + if ($CANames.Count -lt 1) { + Write-Warning 'No certificate authorities were found in Active Directory.' Break } -} + Write-Information "Found: $CANames `n" + + # Get OIDs of all certificate templates that you want to monitor. The script takes MUCH longer when querying by template name. + Write-Information "Getting OIDs for $($TemplateNamesIncluded.Count) certificate templates..." + $TemplateOidsIncluded = foreach ($item in $TemplateNamesIncluded) { + try { + (Get-CertificateTemplate -DisplayName $item).Oid.Value + } catch { + Write-Warning 'Unable to get the certificate templates. Please review the error and try again.' + Write-Error $error + Break + } + } -# Get all relevant certificates that are expiring within the next 45 days. -Write-Information `n"Getting certifictes that are expiring in the next $DaysLeft days from $CANames..." -$Certificates = ( Get-IssuedRequest -CertificationAuthority $CANames -Property [Request.RequesterName], CertificateHash -Filter $CertAgeFilter ).Where( - { $TemplateOidsIncluded -imatch $_.CertificateTemplate } ) | - Select-Object *, $CaName, $CertTemplateName, @{Name="Thumbprint"; Expression = { $_.CertificateHash -Replace (' ','') } } + # Get all relevant certificates that are expiring within the next 45 days. + Write-Information `n"Getting certifictes that are expiring in the next $DaysLeft days from $CANames..." + $Certificates = ( Get-IssuedRequest -CertificationAuthority $CANames -Property [Request.RequesterName], CertificateHash -Filter $CertAgeFilter ).Where( + { $TemplateOidsIncluded -imatch $_.CertificateTemplate } ) | + Select-Object *, $CaName, $CertTemplateName, @{Name = 'Thumbprint'; Expression = { $_.CertificateHash -Replace (' ', '') } } # In the above line, $CaName and $CertTemplateName are "shortcut snippet variables" like a function that formats or translates the desired output. -if ($certificates.Length -eq 0) { - Write-Information "No certificates were found to report on." - Break -} -else { - Write-Information "Found $($Certificates.Count) certificates that expire within $DaysLeft days..." -} + if ($certificates.Length -eq 0) { + Write-Information 'No certificates were found to report on.' + Break + } else { + Write-Information "Found $($Certificates.Count) certificates that expire within $DaysLeft days..." + } -# Done getting the certificates. ║ -# ═══════════════════════════════╝ - - -# ═══════════════════════════╗ -# Build and send the report: ║ - -# Create a structured table with certificate details. Designed specifically for an HTML-based email. -$table = [System.Data.DataTable]::New("CertificatesTable") -@( - "Name" - "Expiration" - "Identifiers" - "Requester" - "CA" -) | ForEach-Object { $table.Columns.Add($_) | Out-Null } - -# Add each certificate to the table -foreach ($cert in $certificates) { - $certRow = $table.NewRow() - $certRow.Name = "$($cert.CommonName)╗($($cert.Templatename))" - $certRow.Expiration = $cert.NotAfter - $certRow.Requester = $cert."Request.RequesterName" - $certRow.Identifiers = "Serial: $($cert.SerialNumber)╗Thumbprint: $($cert.Thumbprint)╗Request ID: $($cert.RequestID)" - $certRow.CA = $cert.CA - $table.Rows.Add($CertRow) -} + # Done getting the certificates. ║ + # ═══════════════════════════════╝ + + + # ═══════════════════════════╗ + # Build and send the report: ║ + + # Create a structured table with certificate details. Designed specifically for an HTML-based email. + $table = [System.Data.DataTable]::New('CertificatesTable') + @( + 'Name' + 'Expiration' + 'Identifiers' + 'Requester' + 'CA' + ) | ForEach-Object { $table.Columns.Add($_) | Out-Null } + + # Add each certificate to the table + foreach ($cert in $certificates) { + $certRow = $table.NewRow() + $certRow.Name = "$($cert.CommonName)╗($($cert.Templatename))" + $certRow.Expiration = $cert.NotAfter + $certRow.Requester = $cert.'Request.RequesterName' + $certRow.Identifiers = "Serial: $($cert.SerialNumber)╗Thumbprint: $($cert.Thumbprint)╗Request ID: $($cert.RequestID)" + $certRow.CA = $cert.CA + $table.Rows.Add($CertRow) + } -$HtmlHeader = @" + $HtmlHeader = @' -"@ -$PreContent = "Internally-issued certificates that will expire in the next $DaysLeft days: ╗╗" -$EmailHtml = ($table | ConvertTo-Html -Head $HtmlHeader -PreContent $PreContent -Property Name,Expiration,Identifiers,Requester,CA | Out-String).Replace('╗','
') -$Subject = "$Domain Certificate Expiration Report" -Send-MailMessage -To $To -From $From -SmtpServer $SMTPServer -Subject $Subject -Body $EmailHtml -BodyAsHTML +'@ + $PreContent = "Internally-issued certificates that will expire in the next $DaysLeft days: ╗╗" + $EmailHtml = ($table | ConvertTo-Html -Head $HtmlHeader -PreContent $PreContent -Property Name, Expiration, Identifiers, Requester, CA | Out-String).Replace('╗', '
') + $Subject = "$Domain Certificate Expiration Report" + Send-MailMessage -To $To -From $From -SmtpServer $SMTPServer -Subject $Subject -Body $EmailHtml -BodyAsHtml +}