Public/Search-PWSHCertutilCerts.ps1

function Search-PWSHCertutilCerts {
    <#
    .SYNOPSIS
        Searches for issued and/or revoked certificates across all CAs in a profile.
    .DESCRIPTION
        Builds a dynamic certutil -restrict string from the supplied filter parameters and
        queries every CA in the profile. Multiple values for the same parameter are combined
        with OR logic. Results include Profile and CAServer metadata. The out columns come
        from the profile's certutilView.out.search configuration, read at call time.
    .PARAMETER Profile
        The configuration profile to use.
    .PARAMETER Type
        Which certificates to search: Issued, Revoked, or All. Default: All.
    .PARAMETER Requester
        One or more requester names to match (OR). Certutil wildcards supported.
    .PARAMETER Subject
        One or more CommonName values to match (OR).
    .PARAMETER Template
        One or more certificate template names to match (OR).
    .PARAMETER NotBefore
        Return only certificates issued on or after this date.
    .PARAMETER NotAfter
        Return only certificates expiring on or before this date.
    .PARAMETER SerialNumber
        One or more serial numbers to match (OR).
    .PARAMETER CAFqdn
        Optional. Queries only this CA instead of all CAs in the profile.
    .PARAMETER Credential
        Optional PSCredential for WinRM. Defaults to current user.
    .EXAMPLE
        Search-PWSHCertutilCerts -Profile 'prod-pki' -Subject 'server01.corp.local'
        Finds all (issued + revoked) certificates for subject server01.corp.local.
    .EXAMPLE
        Search-PWSHCertutilCerts -Profile 'prod-pki' -Type Issued -Template 'WebServer','Workstation'
        Finds issued certificates using the WebServer or Workstation template.
    .EXAMPLE
        Search-PWSHCertutilCerts -Profile 'prod-pki' -Type Issued -NotAfter (Get-Date).AddDays(60)
        Finds issued certificates expiring within 60 days.
    .OUTPUTS
        PSCustomObject[]. One object per matching certificate with Profile, CAServer, and all configured search -out fields.
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory, Position = 0)]
        [string] $Profile,

        [Parameter()]
        [ValidateSet('Issued', 'Revoked', 'All')]
        [string] $Type = 'All',

        [Parameter()]
        [string[]] $Requester,

        [Parameter()]
        [string[]] $Subject,

        [Parameter()]
        [string[]] $Template,

        [Parameter()]
        [datetime] $NotBefore,

        [Parameter()]
        [datetime] $NotAfter,

        [Parameter()]
        [string[]] $SerialNumber,

        [Parameter()]
        [string] $CAFqdn,

        [Parameter()]
        [pscredential] $Credential
    )

    $config        = Read-ConfigFile
    $profileConfig = Get-ProfileConfig -Config $config -ProfileName $Profile

    $autoSyncArgs = @{ Config = $config; ProfileName = $Profile; ProfileConfig = $profileConfig }
    if ($PSBoundParameters.ContainsKey('Credential')) { $autoSyncArgs['Credential'] = $Credential }
    $profileConfig = Invoke-ProfileAutoSync @autoSyncArgs

    $fieldMap = @{}
    if ($profileConfig.syncState -and $profileConfig.syncState.fieldNameMap) {
        $profileConfig.syncState.fieldNameMap.PSObject.Properties |
            ForEach-Object { $fieldMap[$_.Name] = $_.Value }
    }

    $parts = [System.Collections.Generic.List[string]]::new()
    switch ($Type) {
        'Issued'  { $parts.Add('Disposition=20') }
        'Revoked' { $parts.Add('Disposition=21') }
    }
    if ($Requester)    { $parts.Add(($Requester    | ForEach-Object { "RequesterName=$_"       }) -join '|') }
    if ($Subject)      { $parts.Add(($Subject       | ForEach-Object { "CommonName=$_"          }) -join '|') }
    if ($Template)     { $parts.Add(($Template      | ForEach-Object { "CertificateTemplate=$_" }) -join '|') }
    if ($SerialNumber) { $parts.Add(($SerialNumber  | ForEach-Object { "SerialNumber=$_"        }) -join '|') }
    if ($PSBoundParameters.ContainsKey('NotBefore')) {
        $parts.Add("NotBefore>=$($NotBefore.ToString('MM\/dd\/yyyy'))")
    }
    if ($PSBoundParameters.ContainsKey('NotAfter')) {
        $parts.Add("NotAfter<=$($NotAfter.ToString('MM\/dd\/yyyy'))")
    }

    $restrict = if ($parts.Count -gt 0) { $parts -join ',' } else { 'GeneralFlags=0' }
    $out      = ($profileConfig.certutilView.out.search) -join ','

    $cas = if ($PSBoundParameters.ContainsKey('CAFqdn')) {
        $found = $profileConfig.cas | Where-Object { $_.fqdn -eq $CAFqdn }
        if (-not $found) { throw "CA '$CAFqdn' is not defined in profile '$Profile'." }
        $found
    } else { $profileConfig.cas }

    foreach ($ca in $cas) {
        try {
            $sessionArgs = @{ CAFqdn = $ca.fqdn; RemotingConfig = $profileConfig.remoting }
            if ($PSBoundParameters.ContainsKey('Credential')) { $sessionArgs['Credential'] = $Credential }

            $session   = Get-CASession @sessionArgs
            $rawOutput = Invoke-CertutilView -Session $session -Restrict $restrict -Out $out
            ConvertFrom-CertutilCsv -RawOutput $rawOutput -FieldMap $fieldMap |
                Add-ResultMetadata -Profile $Profile -CAServer $ca.fqdn
        } catch {
            Write-Error "Failed to search certs on '$($ca.fqdn)': $_"
        }
    }
}