public/Get-MsecDefenderCertificateUsage.ps1

function Get-MsecDefenderCertificateUsage {
    <#
    .SYNOPSIS
        Code-signing certificates actually in use on the fleet, with their validity window, how
        many devices carry files signed by each, and how long each has left.

    .DESCRIPTION
        Reads DeviceFileCertificateInfo through advanced hunting and groups it by signing
        certificate rather than by file, so the question becomes "whose code are we running, and
        on what trust" instead of "what is this binary".

        SIGNERHASH IS THE WINDOWS THUMBPRINT, AND IS THE JOIN TO DEFENDER INDICATORS. The SHA-1
        over the certificate's DER bytes is what Windows calls the thumbprint, what Defender
        reports as SignerHash, and what New-MsecDefenderIndicator takes as -Value for a
        CertificateThumbprint indicator. Verified both ways on one tenant: the value Defender
        reported and the SHA-1 computed from the vendor's own installer were identical. That
        equality is the reason this command is useful rather than merely interesting - the
        thumbprint needed to allow or block a publisher is in the output, so nothing has to be
        downloaded, unpacked, or extracted from a binary.

        A CERTIFICATE EXPIRY IS A ROTATION, AND A ROTATION BREAKS INDICATORS. Only LEAF
        certificates can be used in a Defender indicator; parents and children are not included.
        So when a publisher renews, everything they sign afterwards carries a new thumbprint that
        existing indicators do not match - while the old indicator keeps working for everything
        already signed, because timestamped Authenticode signatures stay valid past expiry. The
        gap is therefore silent and one-directional: old files keep working, new ones stop.
        DaysUntilExpiry is how far away that is, and it is why this command sorts by it.

        TRUST IS REPORTED, NOT FILTERED ON. An untrusted or self-signed certificate running on
        managed devices is a finding, not noise to be hidden, so IsTrusted is a column and
        everything is returned by default. -TrustedOnly is there for when you are specifically
        building an allowlist and want to be sure you are not about to allow something the
        platform already distrusts.

        THE WINDOW IS THE HUNTING WINDOW, WHICH IS NOT THE SAME AS "IN USE". Advanced hunting
        retains 30 days and measured as little as 7 on one tenant. A certificate absent from
        this output has not been seen signing a file that ran recently; it has not necessarily
        been retired. The window actually returned is reported on the verbose stream.

    .PARAMETER Signer
        Substring match on the signing subject, case-insensitive. 'anthropic', 'microsoft'.

    .PARAMETER Issuer
        Substring match on the issuing CA. Useful for separating platform signing identities
        from Authenticode ones - an Apple 'Developer ID Certification Authority' certificate is
        not usable in a Defender indicator, which is Windows-only.

    .PARAMETER ExpiringWithinDays
        Only certificates expiring within this many days. The rotation warning.

    .PARAMETER TrustedOnly
        Only certificates the platform reports as trusted.

    .PARAMETER Days
        Hunting window to search. Default 30, which is the retention ceiling - asking for more
        cannot find more.

    .EXAMPLE
        Get-MsecDefenderCertificateUsage -Signer anthropic

        Every Anthropic signing certificate seen on the fleet. SignerHash is the value to hand
        to New-MsecDefenderIndicator.

    .EXAMPLE
        Get-MsecDefenderCertificateUsage -ExpiringWithinDays 60 |
            Sort-Object DaysUntilExpiry |
            Format-Table Signer, DaysUntilExpiry, Devices, SignerHash

        Publishers about to rotate. Any certificate here that an indicator depends on needs a
        replacement indicator when the new one appears.

    .EXAMPLE
        Get-MsecDefenderCertificateUsage | Where-Object { -not $_.IsTrusted }

        Untrusted or self-signed code running on managed devices.

    .EXAMPLE
        # The rotation check, run daily against a list of thumbprints already approved.
        $approved = Get-Content ./approved-thumbprints.txt
        Get-MsecDefenderCertificateUsage -Signer anthropic |
            Where-Object { $_.SignerHash -notin $approved }

        Rows mean a new signing certificate has landed and an indicator needs adding. Keeping
        the approved list in version control makes it the exception register as well as the
        comparison set.

    .OUTPUTS
        PSCustomObject per certificate, PSTypeName 'MsecDefenderCertificateUsage'.

    .NOTES
        Needs 'ThreatHunting.Read.All', which New-MsecApp already grants - this runs as the app.
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [string] $Signer,

        [string] $Issuer,

        [int] $ExpiringWithinDays,

        [switch] $TrustedOnly,

        [ValidateRange(1, 30)]
        [int] $Days = 30
    )

    Assert-MsecSession

    $query = @"
DeviceFileCertificateInfo
| where Timestamp > ago(${Days}d)
| where isnotempty(Signer)
| summarize Files = dcount(SHA1), Devices = dcount(DeviceId),
            FirstSeen = min(Timestamp), LastSeen = max(Timestamp),
            IsTrusted = max(todouble(IsTrusted)),
            IsRootSignerMicrosoft = max(todouble(IsRootSignerMicrosoft))
          by Signer, Issuer, SignerHash, IssuerHash, CertificateSerialNumber,
             CertificateCreationTime, CertificateExpirationTime
| order by Devices desc
"@


    $rows = @(Search-MsecDefenderHunting -Query $query)
    if (-not $rows.Count) {
        Write-Warning "No certificate records returned for the last $Days day(s). DeviceFileCertificateInfo is populated by file events, so an empty result can also mean advanced hunting holds less history than was asked for - it is NOT evidence that nothing is signed."
        return
    }

    $seen = @($rows | ForEach-Object { if ($_.FirstSeen) { [datetime] $_.FirstSeen } }) | Sort-Object
    if ($seen.Count) {
        Write-Verbose "Asked for $Days day(s); $($rows.Count) certificate(s) across a window starting $($seen[0].ToString('u'))."
    }

    $now = (Get-Date).ToUniversalTime()

    foreach ($r in $rows) {
        if ($Signer -and "$($r.Signer)" -notmatch [regex]::Escape($Signer)) { continue }
        if ($Issuer -and "$($r.Issuer)" -notmatch [regex]::Escape($Issuer)) { continue }

        # Defender returns these as sbyte 0/1 through the OData surface, and max() above makes
        # them doubles. Compared against 1 rather than cast to [bool], because [bool] on the
        # STRING "0" is $true - the same trap as Secure Score's 'on' field being the text
        # "false", which reported every control as enabled until it was caught.
        $trusted = ($null -ne $r.IsTrusted) -and ([double] $r.IsTrusted -eq 1)
        if ($TrustedOnly -and -not $trusted) { continue }

        $validTo = if ($r.CertificateExpirationTime) { [datetime] $r.CertificateExpirationTime } else { $null }
        # $null, never a large number, when the certificate carries no expiry in the record.
        # A missing expiry sorted alongside real ones would put "unknown" at the safe end.
        $daysLeft = if ($validTo) { [math]::Round(($validTo.ToUniversalTime() - $now).TotalDays, 1) } else { $null }

        if ($PSBoundParameters.ContainsKey('ExpiringWithinDays')) {
            if ($null -eq $daysLeft -or $daysLeft -gt $ExpiringWithinDays) { continue }
        }

        [PSCustomObject]@{
            PSTypeName            = 'MsecDefenderCertificateUsage'
            Signer                = [string] $r.Signer
            Issuer                = [string] $r.Issuer
            # The Windows thumbprint. Hand this straight to New-MsecDefenderIndicator.
            SignerHash            = [string] $r.SignerHash
            IssuerHash            = [string] $r.IssuerHash
            SerialNumber          = [string] $r.CertificateSerialNumber
            ValidFrom             = if ($r.CertificateCreationTime) { [datetime] $r.CertificateCreationTime } else { $null }
            ValidTo               = $validTo
            DaysUntilExpiry       = $daysLeft
            Expired               = if ($null -eq $daysLeft) { $null } else { $daysLeft -lt 0 }
            Devices               = [int] $r.Devices
            Files                 = [int] $r.Files
            IsTrusted             = $trusted
            IsRootSignerMicrosoft = ($null -ne $r.IsRootSignerMicrosoft) -and ([double] $r.IsRootSignerMicrosoft -eq 1)
            FirstSeen             = if ($r.FirstSeen) { [datetime] $r.FirstSeen } else { $null }
            LastSeen              = if ($r.LastSeen) { [datetime] $r.LastSeen } else { $null }
            Raw                   = $r
        }
    }
}