Collectors/Get-EscUnusedPublishedTemplate.ps1
|
function Get-EscUnusedPublishedTemplate { <# .SYNOPSIS Lists CA-published templates that have NO still-valid issued certificate (read-only, live). Candidates to unpublish and shrink attack surface. .DESCRIPTION For each Enterprise CA, correlates: * the templates published on the CA (Enrollment Service certificateTemplates), and * the templates behind currently-valid issued certificates - those whose NotAfter is still in the future - read via 'certutil -view -restrict "Disposition=20,NotAfter>=now" -out CertificateTemplate'. A published template with zero active certificates is reported as unused. The issued certificate's template identifier (an OID for v2+ templates, a name for v1) is resolved back to the template name using the collected template objects (Name / DisplayName / Oid). Read-only: certutil -view only READS the CA DB. .PARAMETER EnrollmentService CA objects from Get-EscEnrollmentService (Name, DnsHostName, PublishedTemplates). .PARAMETER Template Template objects from Get-EscCertificateTemplate (for OID -> name resolution). .OUTPUTS [pscustomobject] per CA: CaName, DnsHostName, PublishedCount, ActiveTemplateCount, Unused[] (template names), Error. #> [CmdletBinding()] param( [Parameter(Mandatory = $false)] [object[]] $EnrollmentService = @(), [Parameter(Mandatory = $false)] [object[]] $Template = @() ) $byId = @{} foreach ($t in $Template) { if ($null -eq $t) { continue } $nm = [string]$t.Name if ([string]::IsNullOrWhiteSpace($nm)) { continue } $byId[$nm.ToLowerInvariant()] = $nm if ($t.DisplayName) { $byId[([string]$t.DisplayName).ToLowerInvariant()] = $nm } if ($t.Oid) { $byId[([string]$t.Oid).ToLowerInvariant()] = $nm } } $resolve = { param($val) $v = [string]$val if ([string]::IsNullOrWhiteSpace($v)) { return $null } $key = $v.Trim().ToLowerInvariant() if ($byId.ContainsKey($key)) { return $byId[$key] } if ($v -match '^\s*(\S+)\s*\((.+)\)\s*$') { $a = $Matches[1].Trim().ToLowerInvariant() $b = $Matches[2].Trim().ToLowerInvariant() if ($byId.ContainsKey($a)) { return $byId[$a] } if ($byId.ContainsKey($b)) { return $byId[$b] } return $Matches[2].Trim() } return $v.Trim() } $perCa = @() foreach ($svc in $EnrollmentService) { if ($null -eq $svc) { continue } $caName = [string]$svc.Name $dns = [string]$svc.DnsHostName $published = @($svc.PublishedTemplates | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) } | Select-Object -Unique) if ($published.Count -eq 0) { continue } $short = ($dns -split '\.')[0] $isLocalCa = [string]::IsNullOrWhiteSpace($dns) -or ($short -ieq $env:COMPUTERNAME) $usedNames = @() $err = '' try { $cuArgs = @() if (-not $isLocalCa -and $caName -and $dns) { $cuArgs += @('-config', ('{0}\{1}' -f $dns, $caName)) } $cuArgs += @('-view', '-restrict', 'Disposition=20,NotAfter>=now', '-out', 'CertificateTemplate', 'csv') $raw = & certutil.exe @cuArgs 2>&1 $text = ($raw | Out-String) if ($LASTEXITCODE -ne 0) { $err = ('certutil -view returned exit {0}.' -f $LASTEXITCODE) } $rows = @() try { $rows = @($text | ConvertFrom-Csv) } catch { $rows = @() } $col = $null if ($rows.Count -gt 0) { $col = ($rows[0].PSObject.Properties.Name | Where-Object { $_ -match 'Certificate ?Template|Template' } | Select-Object -First 1) } $set = @{} foreach ($row in $rows) { $val = '' if ($col) { $val = [string]$row.$col } else { $val = [string]($row.PSObject.Properties.Value | Select-Object -First 1) } $nm = & $resolve $val if ($nm) { $set[$nm.ToLowerInvariant()] = $nm } } $usedNames = @($set.Values) } catch { $err = $_.Exception.Message } $usedLower = @($usedNames | ForEach-Object { ([string]$_).ToLowerInvariant() }) $unused = @($published | Where-Object { $usedLower -notcontains ([string]$_).ToLowerInvariant() } | Sort-Object) $perCa += [pscustomobject]@{ CaName = $caName DnsHostName = $dns PublishedCount = $published.Count ActiveTemplateCount = @($usedNames).Count Unused = @($unused) Error = $err } } Write-EscLog -Component 'TemplateUsage' -Message ("Analyzed {0} CA(s) for unused published templates." -f @($perCa).Count) return @($perCa) } |