Public/Get-CwCertInventory.ps1
|
function Get-CwCertInventory { <# .SYNOPSIS Reads local certificate stores and normalizes certificates into the cert-watchtower model including role classification. .DESCRIPTION Strictly read-only scan of the given stores (default LocalMachine\My). Each certificate is normalized (subject, thumbprint, validity, EKUs, template, SANs) and classified into an operational role (CA/LDAPS/RDP/IIS/Client/CodeSigning/Other). Severity is derived from the remaining lifetime; CA certificates get their own (longer) thresholds. .PARAMETER StoreName One or more store names under the store location. Default: My. .PARAMETER StoreLocation LocalMachine (default) or CurrentUser. .PARAMETER WarnDays Warning threshold in days for non-CA certificates. Default 30. .PARAMETER CritDays Critical threshold in days for non-CA certificates. Default 7. .PARAMETER CaWarnDays Warning threshold for CA certificates. Default 180. .PARAMETER CaCritDays Critical threshold for CA certificates. Default 30. .EXAMPLE Get-CwCertInventory -WarnDays 30 -CritDays 7 | Where-Object Severity -ne 'OK' #> [CmdletBinding()] [OutputType([pscustomobject])] param( [string[]]$StoreName = @('My'), [ValidateSet('LocalMachine', 'CurrentUser')] [string]$StoreLocation = 'LocalMachine', [int]$WarnDays = 30, [int]$CritDays = 7, [int]$CaWarnDays = 180, [int]$CaCritDays = 30 ) $isDc = $false try { $isDc = (Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction Stop).DomainRole -ge 4 } catch { Write-Verbose "Could not determine domain role: $_" } $now = Get-Date foreach ($store in $StoreName) { $path = "Cert:\$StoreLocation\$store" if (-not (Test-Path -Path $path)) { Write-Warning "Certificate store '$path' not found - skipping." continue } foreach ($cert in (Get-ChildItem -Path $path)) { if ($cert -isnot [System.Security.Cryptography.X509Certificates.X509Certificate2]) { continue } $ekuOids = @() $isCa = $false $template = '' $sanNames = @() foreach ($ext in $cert.Extensions) { switch ($ext.Oid.Value) { '2.5.29.37' { $ekuOids = @($ext.EnhancedKeyUsages | ForEach-Object Value) } '2.5.29.19' { $isCa = $ext.CertificateAuthority } '1.3.6.1.4.1.311.21.7' { # msPKI-Certificate-Template: Format() text contains 'Template=<name>(...)' if ($ext.Format($false) -match '(?:Template|Vorlage)\s*=\s*([^,(\r\n]+)') { $template = $Matches[1].Trim() } } '1.3.6.1.4.1.311.20.2' { # legacy V1 certificate template name (BMPString) if (-not $template) { $template = ($ext.Format($false)).Trim() } } '2.5.29.17' { $sanNames = @([regex]::Matches($ext.Format($false), 'DNS[ -]?Name\s*=\s*([^,\s]+)') | ForEach-Object { $_.Groups[1].Value }) } } } $role = Get-CwCertRole -EkuOids $ekuOids -IsCa $isCa -IsDomainController $isDc -TemplateName $template $daysRemaining = [math]::Round(($cert.NotAfter - $now).TotalDays, 2) $sevParams = if ($role -eq 'CA') { @{ WarnDays = $CaWarnDays; CritDays = $CaCritDays } } else { @{ WarnDays = $WarnDays; CritDays = $CritDays } } [pscustomobject]@{ PSTypeName = 'CertWatchtower.Certificate' Subject = $cert.Subject Issuer = $cert.Issuer Thumbprint = $cert.Thumbprint SerialNumber = $cert.SerialNumber NotBefore = $cert.NotBefore NotAfter = $cert.NotAfter DaysRemaining = $daysRemaining HasPrivateKey = $cert.HasPrivateKey Role = $role Template = $template EkuOids = $ekuOids SanDnsNames = $sanNames IsCa = $isCa Store = "$StoreLocation\$store" Severity = ConvertTo-CwSeverity -DaysRemaining $daysRemaining @sevParams } } } } |