Last active
October 1, 2024 20:25
-
-
Save Bill-Stewart/ce9280b1ed796b639c2f92dd6740fb3b to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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