Skip to content

Instantly share code, notes, and snippets.

@jdhitsolutions
Last active November 10, 2023 13:08
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jdhitsolutions/d306861d3c5641504779a46f00fce7f1 to your computer and use it in GitHub Desktop.
Save jdhitsolutions/d306861d3c5641504779a46f00fce7f1 to your computer and use it in GitHub Desktop.
A PowerShell 7 function and custom format file to analyze an event log and report on error sources. Put both files in the same folder. Dot source the ps1 file.
#requires -version 7.2
#requires -module ThreadJob
if ($IsLinux -OR $IsMacOS) {
Return "$($PSStyle.foreground.red)This command requires a Windows platform.$($PSStyle.Reset)"
}
if ($host.name -ne "ConsoleHost") {
Return "$($PSStyle.foreground.red)Detected $($host.name). This command must be run from a PowerShell 7.x console prompt.$($PSStyle.Reset)"
}
Function Get-WinEventReport {
[cmdletbinding()]
[alias("wer")]
[outputType("WinEventReport")]
Param(
[Parameter(
Position = 0,
Mandatory,
ValueFromPipelineByPropertyName,
HelpMessage = "Specify the name of an event log like System."
)]
[ValidateNotNullOrEmpty()]
[ArgumentCompleter({
[OutputType([System.Management.Automation.CompletionResult])]
param(
[string] $CommandName,
[string] $ParameterName,
[string] $WordToComplete,
[System.Management.Automation.Language.CommandAst] $CommandAst,
[System.Collections.IDictionary] $FakeBoundParameters
)
$CompletionResults = [System.Collections.Generic.List[System.Management.Automation.CompletionResult]]::new()
(Get-WinEvent -ListLog *$wordToComplete*).LogName.Foreach({
# completion text,listitem text,result type,Tooltip
$CompletionResults.add($([System.Management.Automation.CompletionResult]::new("'$($_)'", $_, 'ParameterValue', $_) ))
})
return $CompletionResults
})]
[string]$LogName,
[Parameter(HelpMessage = "Specifies the maximum number of events that are returned. Enter an integer such as 100. The default is to return
all the events in the logs or files.")]
[Int64]$MaxEvents,
[Parameter(
ValueFromPipeline,
HelpMessage = "Specifies the name of the computer that this cmdlet gets events from the event logs."
)]
[string[]]$Computername = $env:COMPUTERNAME,
[Parameter(HelpMessage = "This parameter limits the number of jobs running at one time. As jobs are started, they are queued and wait until a thread is available in the thread pool to run the job.")]
[ValidateScript({$_ -gt 0})]
[int]$ThrottleLimit = 5
)
Begin {
Write-Verbose "[$((Get-Date).TimeOfDay) BEGIN ] Starting $($MyInvocation.MyCommand)"
$JobList = [System.Collections.Generic.list[object]]::New()
} #begin
Process {
Write-Verbose "[$((Get-Date).TimeOfDay) PROCESS] Getting eventlog entries from $LogName"
foreach ($computer in $computername) {
$job = {
Param($LogName, $MaxEvents, $computername)
#match verbose preference
$VerbosePreference = $using:VerbosePreference
#remove MaxEvents if there is no value passed
if ($PSBoundParameters["MaxEvents"] -le 1) {
[void]$PSBoundParameters.Remove("MaxEvents")
}
Try {
Write-Verbose "[$((Get-Date).TimeOfDay) THREAD ] Querying $($($PSBoundParameters)["LogName"]) on $($PSBoundParameters["Computername"].ToUpper())"
$logs = Get-WinEvent @PSBound$PSBoundParameters -ErrorAction Stop | Group-Object ProviderName
$logCount = ($logs | Measure-Object -Property Count -Sum).sum
Write-Verbose "[$((Get-Date).TimeOfDay) THREAD ] Retrieved $($logs.count) event sources from $logCount records."
Write-Verbose "[$((Get-Date).TimeOfDay) THREAD ] Detected log $($logs[0].group[0].LogName)"
$logs.foreach({
if ( $_.group[0].LogName -eq 'Security') {
$AS = (($_.group).where({ $_.keywordsDisplaynames[0] -match "Success" }).count)
$AF = (($_.group).where({ $_.keywordsDisplaynames[0] -match "Failure" }).count)
}
else {
$AS = 0
$AF = 0
}
$report = [PSCustomObject]@{
PSTypename = "WinEventReport"
LogName = $_.group[0].LogName
Source = $_.Name
Total = $_.Count
Information = (($_.group).where({ $_.level -eq 4 }).count)
Warning = (($_.group).where({ $_.level -eq 3 }).count)
Error = (($_.group).where({ $_.level -eq 2 }).count)
AuditSuccess = $AS
AuditFailure = $AF
ComputerName = $PSBoundParameters["Computername"].ToUpper()
}
$report
})
} #Try
Catch {
throw "Failed to query $($PSBoundParameters["Computername"].ToUpper()).$($_.Exception.Message)"
}
Write-Verbose "[$((Get-Date).TimeOfDay) THREAD ] Finished processing $($PSBoundParameters["Computername"].ToUpper())"
}
$JobList.Add((Start-ThreadJob -Name $computer -ScriptBlock $job -ArgumentList $LogName, $MaxEvents, $computer -ThrottleLimit $ThrottleLimit))
} #foreach computer
} #process
End {
$count = $JobList.count
do {
Write-Verbose "[$((Get-Date).TimeOfDay) END ] Waiting for jobs to finish: $( $JobList.Where({$_.state -notmatch 'completed|failed'}).Name -join ',')"
[string[]]$waiting = $JobList.Where({ $_.state -notmatch 'completed|failed' }).Name
if ($waiting.count -gt 0) {
#write-progress doesn't display right at 0%
if ($waiting.count -eq $count) {
[int]$pc = 5
}
else {
[int]$pc = (100 - ($waiting.count / $count) * 100)
}
Write-Progress -Activity "Waiting for $($JobList.count) jobs to complete." -Status "$($waiting -join ',') $pc%" -PercentComplete $pc
}
$JobList.Where({ $_.state -match 'completed|failed' }) | ForEach-Object { Receive-Job $_.id -Keep ; [void]$JobList.remove($_) }
#wait 1 second before checking again
Start-Sleep -Milliseconds 1000
} while ($JobList.count -gt 0)
if ($JobList.state -contains 'failed') {
$JobList.Where({ $_.state -match 'failed' }) | ForEach-Object {
$msg = "[{0}] Failed. {1}" -f $_.Name.toUpper(), ((Get-Job -Id $_.id).childjobs.jobstateinfo.reason.message)
Write-Warning $msg
}
}
#The ThreadJobs remain with results so you can retrieve data again.
#You can manually remove the jobs.
Write-Verbose "[$((Get-Date).TimeOfDay) END ] Ending $($MyInvocation.MyCommand)"
} #end
} #close Get-WinEventReport
<#
Import the custom formatting file. It is expected to be in the same
directory as this script file. If querying the Security log you will want
to use the custom view
Get-WinEventReport 'Security' -MaxEvents 1000 -Computername DOM1,DOM2 | format-table -View security
#>
Update-FormatData $PSScriptRoot\WinEventReport.format.ps1xml
<!--
Format type data generated 10/27/2022 15:04:28 by PROSPERO\Jeff
This file was created using the New-PSFormatXML command that is part
of the PSScriptTools module.
https://github.com/jdhitsolutions/PSScriptTools
-->
<Configuration>
<ViewDefinitions>
<View>
<!--Created 10/27/2022 15:04:28 by PROSPERO\Jeff-->
<Name>default</Name>
<ViewSelectedBy>
<TypeName>WinEventReport</TypeName>
</ViewSelectedBy>
<GroupBy>
<!--
You can also use a scriptblock to define a custom property name.
You must have a Label tag.
<ScriptBlock>$_.machinename.toUpper()</ScriptBlock>
<Label>Computername</Label>
Use <Label> to set the displayed value.
-->
<PropertyName>Computername</PropertyName>
<Label>Computername</Label>
</GroupBy>
<TableControl>
<!--Delete the AutoSize node if you want to use the defined widths.-->
<AutoSize />
<TableHeaders>
<TableColumnHeader>
<Label>Logname</Label>
<Width>11</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Source</Label>
<Width>29</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Total</Label>
<Width>8</Width>
<Alignment>right</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Info</Label>
<Width>14</Width>
<Alignment>right</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Warning</Label>
<Width>10</Width>
<Alignment>right</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Error</Label>
<Width>8</Width>
<Alignment>right</Alignment>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<!--
By default the entries use property names, but you can replace them with scriptblocks.
<ScriptBlock>$_.foo /1mb -as [int]</ScriptBlock>
-->
<TableColumnItem>
<PropertyName>Logname</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Source</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Total</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Information</PropertyName>
</TableColumnItem>
<TableColumnItem>
<ScriptBlock>
if (($host.name -match "console|code") -AND ($_.warning -gt 0)) {
<!-- orange-->
"$([char]27)[38;2;241;195;15m$($_.warning)$([char]27)[0m"
}
else {
$_.warning
}
</ScriptBlock>
</TableColumnItem>
<TableColumnItem>
<ScriptBlock>
if (($host.name -match "console|code") -AND ($_.error -gt 0)) {
<!-- red-->
"$([char]27)[38;5;197m$($_.Error)$([char]27)[0m"
}
else {
$_.Error
}
</ScriptBlock>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
<View>
<!--Created 10/27/2022 15:06:11 by PROSPERO\Jeff-->
<Name>security</Name>
<ViewSelectedBy>
<TypeName>WinEventReport</TypeName>
</ViewSelectedBy>
<GroupBy>
<!--
You can also use a scriptblock to define a custom property name.
You must have a Label tag.
<ScriptBlock>$_.machinename.toUpper()</ScriptBlock>
<Label>Computername</Label>
Use <Label> to set the displayed value.
-->
<PropertyName>Computername</PropertyName>
<Label>Computername</Label>
</GroupBy>
<TableControl>
<!--Delete the AutoSize node if you want to use the defined widths.-->
<AutoSize />
<TableHeaders>
<TableColumnHeader>
<Label>Logname</Label>
<Width>11</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Source</Label>
<Width>29</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Total</Label>
<Width>8</Width>
<Alignment>right</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>AuditSuccess</Label>
<Width>15</Width>
<Alignment>right</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>AuditFailure</Label>
<Width>15</Width>
<Alignment>right</Alignment>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<!--
By default the entries use property names, but you can replace them with scriptblocks.
<ScriptBlock>$_.foo /1mb -as [int]</ScriptBlock>
-->
<TableColumnItem>
<PropertyName>Logname</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Source</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Total</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>AuditSuccess</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>AuditFailure</PropertyName>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
</ViewDefinitions>
</Configuration>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment