Skip to content

Instantly share code, notes, and snippets.

@Bill-Stewart
Last active October 1, 2024 20:25
Show Gist options
  • Save Bill-Stewart/ce9280b1ed796b639c2f92dd6740fb3b to your computer and use it in GitHub Desktop.
Save Bill-Stewart/ce9280b1ed796b639c2f92dd6740fb3b to your computer and use it in GitHub Desktop.
#requires -version 5.1
# Get-X509Event.ps1
# Written by Bill Stewart (bstewart AT iname.com)
#
# Gets X.509 "no strong mapping" certificate events from domain controllers.
# See Microsoft article KB5014754 for more information.
#
# Version History
#
# 2024/10/01
# * Added EventID parameter.
# * Clarified synopsis and description.
# * Added EventID and EventLevel properties to output objects.
# * Renamed TimeCreated property to EventTime.
# * Clarified progress and status messages.
# * Added script analyzer ignore rule for PSAvoidDefaultValueSwitchParameter.
#
# 2024/09/30
# * Corrected typos.
#
# 2024/09/26
# * Initial version.
<#
.SYNOPSIS
Gets X.509 "no strong mapping" certificate mapping events from domain controllers.
.DESCRIPTION
Gets X.509 "no strong mapping" certificate mapping events from domain controllers. See Microsoft article KB5014754 for more information.
.PARAMETER CurrentSite
Gets events from domain controllers in the current site in the current domain (default).
.PARAMETER Site
Gets events from domain controllers in the named site(s) in the current domain.
.PARAMETER ComputerName
Gets events from the named domain controller(s) in the current domain.
.PARAMETER All
Gets events from all domain controllers in the current domain.
.PARAMETER EventID
Specifies the event ID. The default event ID is 39, but this should be 41 for Server 2008 R2 SP1 or Server 2008 SP2 domain controllers. See Micrsoft article KB5014754 for more information.
.PARAMETER Days
Gets no more that this many days worth of events. The default is 14 days.
.PARAMETER MaxEvents
Gets no more than this many events. The default is 4096 events.
.PARAMETER RawEvents
Outputs raw Diagnostics.Eventing.Reader.EventRecord objects rather than processed objects.
.OUTPUTS
Without the -RawEvents parameter, outputs objects with the following properties:
* MachineName - Domain controller server where event was created
* EventTime - The event's date/time stamp
* EventID - The event ID (39 or 41)
* EventLevel - The event's severity level
* UserID - sAMAccountName of the AD user account
* CertificateSubject - Certificate subject
* CertificateIssuer - Certificate issuer
* CertificateThumbprint - Certificate thumbprint
* CertificateSerialNumber - Certificate serial number
.LINK
https://support.microsoft.com/en-us/topic/ad2c23b0-15d8-4340-a468-4d4f3b188f16
#>
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidDefaultValueSwitchParameter","")]
[CmdletBinding(DefaultParameterSetName = "CurrentSite")]
param(
[Parameter(ParameterSetName = "CurrentSite")]
[Switch]
$CurrentSite = $true,
[Parameter(ParameterSetName = "Site",Mandatory)]
[ValidateNotNullOrEmpty()]
[String[]]
$Site,
[Parameter(ParameterSetName = "ComputerName",Mandatory)]
[ValidateNotNullOrEmpty()]
[String[]]
$ComputerName,
[Parameter(ParameterSetName = "All",Mandatory)]
[Switch]
$All,
[Int]
[ValidateSet(39,41)]
$EventID = 39,
[Int]
[ValidateRange(1,[Int]::MaxValue)]
$Days = 14,
[Int]
[ValidateRange(1,[Int]::MaxValue)]
$MaxEvents = 4096,
[Switch]
$RawEvents
)
Import-Module ActiveDirectory -ErrorAction Stop
$ScriptName = $MyInvocation.MyCommand.Name
# Each Diagnostics.Eventing.Reader.EventLogRecord object returned from
# Get-WinEvent has a Properties property that contains the details of the
# certificate mapping. The Value property contains a a list of strings that
# populate the Message property of the event record. By index, these are:
# 0 - sAMAccountName of the account
# 1 - Certificate subject
# 2 - Certificate issuer name
# 3 - Certificate serial number
# 4 - Certificate thumbprint
$EventPropertyIndexUserID = 0
$EventPropertyIndexSubject = 1
$EventPropertyIndexIssuer = 2
$EventPropertyIndexSerial = 3
$EventPropertyIndexThumbprint = 4
# Outputs the current AD site name
function Get-ADSiteName {
[__ComObject].InvokeMember("SiteName","GetProperty",$null,(New-Object -ComObject ADSystemInfo),$null)
}
$DCNames = New-Object Collections.Generic.List[String]
switch ( $PSCmdlet.ParameterSetName ) {
"CurrentSite" {
$DCs = Get-ADDomainController -Filter "Site -eq '$(Get-ADSiteName)'"
if ( $null -eq $DCs ) {
throw "Unable to get domain controllers in current site."
}
foreach ( $DC in $DCs ) {
$DCNames.Add($DC.Name)
}
}
"Site" {
foreach ( $SiteItem in $Site ) {
$DCs = Get-ADDomainController -Filter "Site -eq '$SiteItem'"
if ( $null -eq $DCs ) {
Write-Error "Unable to get domain controllers in site '$SiteItem'." -Category ObjectNotFound
}
else {
foreach ( $DC in $DCs ) {
$DCNames.Add($DC.Name)
}
}
}
}
"ComputerName" {
foreach ( $ComputerNameItem in $ComputerName ) {
$DCNames.Add($ComputerNameItem)
}
}
"All" {
$DCs = Get-ADDomainController -Filter "*"
if ( $null -eq $DCs ) {
throw "Unable to get domain controllers in the current domain."
}
foreach ( $DC in $DCs ) {
$DCNames.Add($DC.Name)
}
}
}
# Nothing to do...
if ( $DCNames.Count -eq 0 ) {
return
}
# Use Hashtable to filter out duplicate user IDs
$EventsByUserID = @{}
# Collect events from specified domain controllers
foreach ( $DCName in $DCNames ) {
$FilterHashtable = @{
"LogName" = "System"
"ID" = $EventID
"StartTime" = (Get-Date).AddDays(-$Days)
}
$Params = @{
"ComputerName" = $DCName
"FilterHashtable" = $FilterHashtable
"MaxEvents" = $MaxEvents
"ErrorAction" = [Management.Automation.ActionPreference]::SilentlyContinue
}
$ProgressStatus = "Getting X.509 certificate event ID $EventID events from '$DCName'"
Write-Progress $ScriptName $ProgressStatus
$Events = Get-WinEvent @Params |
Where-Object { $_.ProviderName -eq "Microsoft-Windows-Kerberos-Key-Distribution-Center" }
Write-Progress $ScriptName -Completed
if ( $null -eq $Events ) {
Write-Host "INFORMATIONAL: No X.509 certificate event ID $EventID event(s) found on '$DCName'."
}
else {
$I = 0
foreach ( $Event in $Events ) {
# Get the sAMAccountName associated with the event
$UserID = $Event.Properties.Value[$EventPropertyIndexUserID]
if ( $null -ne $UserID ) {
# We haven't seen the sAMAccountName yet so add to the hashtable
if ( -not $EventsByUserID.ContainsKey($UserID) ) {
$EventsByUserID.Add($UserID,$Event)
}
# If the current event is newer, replace the EventLogRecord
elseif ( $Event.TimeCreated -gt $EventsByUserID[$UserID].TimeCreated ) {
$EventsByUserID[$UserID] = $Event
}
}
$I++
if ( $I % 500 -eq 0 ) {
Write-Progress $ScriptName $ProgressStatus -PercentComplete ((($I / $Events.Count) * 100) -as [Int])
}
}
Write-Progress $ScriptName -Completed
}
}
$Pipeline = [ScriptBlock]::Create('$EventsByUserID.GetEnumerator() | Select-Object -ExpandProperty Value | Sort-Object TimeCreated -Descending')
if ( $RawEvents ) {
& $Pipeline
}
else {
& $Pipeline | ForEach-Object {
[PSCustomObject] @{
"MachineName" = $_.MachineName
"EventTime" = $_.TimeCreated
"EventID" = $_.Id
"EventLevel" = [Diagnostics.Eventing.Reader.StandardEventLevel] $_.Level
"UserID" = $_.Properties.Value[$EventPropertyIndexUserID]
"CertificateSubject" = $_.Properties.Value[$EventPropertyIndexSubject]
"CertificateIssuer" = $_.Properties.Value[$EventPropertyIndexIssuer]
"CertificateThumbprint" = $_.Properties.Value[$EventPropertyIndexThumbprint]
"CertificateSerialNumber" = $_.Properties.Value[$EventPropertyIndexSerial]
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment