Last active
November 10, 2023 13:08
-
-
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.
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 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 | |
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
<!-- | |
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