For a number of years I wrote the popular Mr. Roboto column for REDMOND magazine. When I first started the column, many of my scripts were written in VBScript. Then as PowerShell came along that became the preferred tool. Over time I realized there were some VBScripts that could be rewritten and even improved using PowerShell. One of them was a script that queried event logs on computers for errors and warnings in the last X number of hours and prepared a colorized HTML report. I revised that script to a PowerShell v1 script. Recently, a loyal reader asked about revising the script again for PowerShell 2.0. That seemed reasonable so here is my revision.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
The first PowerShell version was published early last year. As I looked to revise it I decided to make some changes. First off, I decided to drop all the code for emailing the report. At the time, there were no cmdlets for sending mail, but now we have Send-MailMessage. Instead, I decided to simply create the HTML file. If you need to mail it, you can handle that separately. My code is now an advanced function called New-EventReport.
[cc lang="PowerShell"]
Function New-EventReport {
<# .Synopsis Create an HTML report of selected events. .Description This function will connect to one or more machines and query all eventlogs, looking for critical events, errors, warnings and audit failures that have been recorded in the last X number of hours. The default value is 24. The function defines an embedded style sheet to color code the different types of events. .Parameter Computername The name of the computer to query. The default is the local computer. .Parameter Hours The number of hours from the current date and time to search. The default is 24. .Parameter Path The filename and path for the HTML report. One file is created for all computers piped into the function. The default is EventLogReport.html in the %TEMP% folder. .Parameter Credential A stored PSCredential for alternate authentication .Example PS C:\> New-EventReport
Directory: C:\Users\Jeff\AppData\Local\Temp
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 9/15/2010 11:33 AM 52526 EventLogReport.html
Create a report for the local machine for the last 24 hours.
.Example
PS C:> get-content c:\work\computers.txt | New-EventReport -path c:\reports\daily.html -credential $Admin
Using a saved PSCredential, this will create a new html report for every computer in computers.txt.
.Example
PS C:\> $rpt = get-content c:\work\computers.txt | New-EventReport -path c:\reports\daily.html
PS C:\> Send-MailMessage -to [email protected] -Subject "Eventlog report" -from "[email protected]" -body (get-
content $rpt | Out-String) -bodyasHTML -SmtpServer mail.mycompany.com
Create an eventlog report for all the systems in Computers.txt and then email the report as an HTML message to
[email protected].
.Notes
NAME: New-EventReport
VERSION: 2.1
AUTHOR: Jeffery Hicks
LASTEDIT: 09/20/2010
Learn more with a copy of Windows PowerShell 2.0: TFM (SAPIEN Press 2010)
.Link
Http://jdhitsolutions.com/blog
.Link
Get-Eventlog
Get-WinEvent
.Inputs
Strings for computer names
.Outputs
A file object for the HTML report.
#>
[cmdletbinding()]
Param(
[Parameter(Position=0,ValueFromPipeline=$True)]
[string[]]$Computername=$env:computername,
[Parameter(Position=1)]
[int]$Hours=24,
[Parameter(Position=2)]
[string]$Path="$env:temp\EventLogReport.html",
[object]$credential
)
BEGIN {
Write-Verbose "Starting $($myinvocation.mycommand)"
#define some variables for Write-Progress
$Activity=$myinvocation.mycommand
$Status="Task Setup"
$Current="defining variables"
Write-Progress -Activity $Activity -Status $status -CurrentOperation $current
#delete report file if it exists
if ((Get-Item $Path -ea "Silentlycontinue").Exists) {
Write-Verbose "Deleting $Path"
Remove-Item $Path
}
#convert credential to a PSCredential if a string was passed.
if ( $credential -is [system.management.automation.psCredential]) {
Write-Verbose "Using PSCredential for $($credential.username)"
}
ElseIf ($Credential) {
Write-Verbose "Getting PSCredential for $credential"
$Credential=Get-Credential $credential
}
#convert hours to milliseconds for XML query
$cutoff=$hours*3600*1000
#get the cutoff date
$cutoffDate=(get-date).AddMilliseconds(-$cutoff)
#this will be the report title
$title="Event Log Report: {0} to {1} " -f $cutoffDate,(Get-Date)
#define an embedded style sheet
$style="
"
#insert the title into the header
$header="
$style"
#create variable to hold all results
$all=@()
} #end Begin scriptblock
PROCESS {
#my error handling traps
Trap {
if ($_.Exception -match "RPC server is unavailable") {
Write-Warning "$computername is not available via RPC."
Write-Progress -Activity $activity -Status "Error" -Completed
continue
}
elseif ($_.Exception -match "access is denied") {
Write-Warning "Access denied to $computername."
Write-Progress -Activity $activity -Status "Error" -Completed
continue
}
else {
Write-Warning "There was an error with $computername"
Write-Warning $_
Write-Progress -Activity $activity -Status "Error" -Completed
continue
}
}
foreach ($computer in $computername) {
$status="Getting event log data from $computer"
Write-Progress -Activity $activity -Status $status -CurrentOperation "Listing logs"
$results=@()
#define some scriptblocks
$list={Param($computername) get-winevent -listlog * -computername $computername}
$listPriv={Param($computername,$credential) get-winevent -listlog * -computername $computername -credential $credential}
Write-Verbose "Getting Event logs from $computername"
if ($credential) {
Write-Verbose "Executing $listpriv"
$logs=&$listpriv $computer $credential
}
else {
Write-Verbose "Executing $list"
$logs=&$list $computer
}
Write-Verbose "Returned a total of $($logs.count) logs"
$logs | where {$_.recordcount -gt 0} |
foreach {
$log=$_.logname
write-verbose "Analyzing $log"
if ($log -eq "Security") {
Write-Verbose "Security log query"
$xmlQuery="
}
else {
$xmlQuery="
}
$cmd='get-winevent -computername $computer -FilterXml $xmlQuery -ErrorAction "SilentlyContinue"'
if ($credential) {
$cmd=$cmd + " -credential `$credential"
}
#get matching Event logs
Write-Progress -Activity $activity -Status $status -CurrentOperation "Querying event logs"
$results+=Invoke-Expression $cmd |
Select Machinename,LevelDisplayName,TimeCreated,ProviderName,Id,Message,LogName
} #foreach eventlog
if ($results.count -gt 0) {
Write-Verbose "Returned $($results.count) events for $($computer)"
$all+=$results
}
else {
Write-warning "No matching events found for $computer"
}
} #foreach computer
} #end Process scriptblock
END {
if ($all.count -gt 0) {
$status="Post processing"
$footer="
Report generated {0} by {1}\{2}" -f (Get-Date),$env:userdomain,$env:username
#convert running results to an HTML file
Write-Verbose "Converting to HTML"
Write-Progress -Activity $activity -Status $status -CurrentOperation "Converting to HTML"
$html = $all | ConvertTo-Html -head $header
#parse HTML file and add color highlighting
$colorized=@()
#define a counter
$i=0
foreach ($line in $html) {
$i++
Write-Progress -Activity $activity -Status $status -CurrentOperation "Colorizing" -PercentComplete $($i/($html.count)*100)
Switch -regex ($line) {
"
" {
Write-Verbose "Colorizing header"
$colorized+=$line.Replace("
}
"
" {
Write-Verbose "Colorizing Error"
$colorized+=$line.Replace("
}
"
" {
Write-Verbose "Colorizing Critical"
$colorized+=$line.Replace("
}
"
" {
Write-Verbose "Colorizing Audit Failure"
$colorized+=$line.Replace("
}
"
" {
Write-Verbose "Adding footer $($footer)"
$colorized+=$line.Replace("
",$footer)
}
Default {
$colorized+=$line
}
} #end Switch
}
Write-Verbose "Sending colorized output to $($Path)"
$colorized | Out-File $Path
Write-Progress -Activity $activity -Status "Finished report" -CurrentOperation $path -Complete
#write the html file object to the pipeline
Get-Item -Path $Path
}
else {
Write-Host "Finished. No results found." -foregroundcolor Magenta
}
} #end End scriptblock
} #function
[/cc]
The function takes one or more computernames, they can also be piped in. For each computer, the function uses the new cmdlet Get-Winevent to list all the logs on the computer. For new machines this will return all the new event logs as well as the classic.
[cc lang="PowerShell"]
$list={Param($computername) get-winevent -listlog * -computername $computername}
[/cc]
To keep things speedy, I ignore and logs that have no entries and only process those that do have entries.
[cc lang="PowerShell"]
Write-Verbose "Returned a total of $($logs.count) logs"
$logs | where {$_.recordcount -gt 0} |
foreach {
$log=$_.logname
write-verbose "Analyzing $log"
[/cc]
The query is for errors, warnings, critical events and audit failures for the last X number of hours that you specify. The default is 24 hours. Because of the new event logs, I use XML filtering queries. The security event log is slightly different so that required a separated XML query.
[cc lang="PowerShell"]
if ($log -eq "Security") {
Write-Verbose "Security log query"
$xmlQuery="
}
else {
$xmlQuery="
}
[/cc]
The function constructs a command and executes it using Invoke-Command.
[cc lang="PowerShell"]
$cmd='get-winevent -computername $computer -FilterXml $xmlQuery -ErrorAction "SilentlyContinue"'
if ($credential) {
$cmd=$cmd + " -credential `$credential"
}
#get matching Event logs
Write-Progress -Activity $activity -Status $status -CurrentOperation "Querying event logs"
$results+=Invoke-Expression $cmd |
Select Machinename,LevelDisplayName,TimeCreated,ProviderName,Id,Message,LogName
[/cc]
Results from all computers are stored in the same variable, $results. This means you get a single HTML report for all computers, but it includes the computername. Now the fun part.
Converting the results to HTML is pretty easy with ConvertTo-HTML. Even adding a style sheet is much easier in 2.0. However, I wanted rows with different type of events to have a different color background. Warnings have no color. To accomplish this the best way I know is to define an HTML class for each row. Then I could use a style sheet to apply style, including color, to each appropriate class. I decided to embed the stylesheet in the HTML header to keep everything self-contained, but you could certainly reference an external style sheet. I'm sure it's clear I'm not a web developer.
[cc lang="PowerShell"]
#define an embedded style sheet
$style="
"
#insert the title into the header
$header="
$style"
[/cc]
After I convert the results to HTML, the function goes through each line, looking for specific identifying information using regular expressions.
[cc lang="PowerShell"]
foreach ($line in $html) {
$i++
Write-Progress -Activity $activity -Status $status -CurrentOperation "Colorizing" -PercentComplete $($i/($html.count)*100)
Switch -regex ($line) {
"
" {
Write-Verbose "Colorizing header"
$colorized+=$line.Replace("
}
"
" {
Write-Verbose "Colorizing Error"
$colorized+=$line.Replace("
}
"
" {
Write-Verbose "Colorizing Critical"
$colorized+=$line.Replace("
}
"
" {
Write-Verbose "Colorizing Audit Failure"
$colorized+=$line.Replace("
}
"
" {
Write-Verbose "Adding footer $($footer)"
$colorized+=$line.Replace("
",$footer)
}
Default {
$colorized+=$line
}
} #end Switch
[/cc]
This is what permits me to define HTML classes. In essence, I'm tweaking HTML on the fly.
The last part is to save the HTML to a file. The default is %TEMP%\EventLogReport.html. The function writes the file object to the pipeline. That's so you can use it like this:
[cc lang="PowerShell"]
PS C:\> $rpt = get-content c:\work\computers.txt | New-EventReport -path c:\reports\daily.html
PS C:\> Send-MailMessage -to [email protected] -Subject "Eventlog report" -from "[email protected]" -body (get-content $rpt | out-string) -bodyasHTML -SmtpServer mail.mycompany.com
[/cc]
The function's output is saved to a variable, and the file contents are used as HTML body for the mail message. The function also uses Write-Progress since this could be a long running script.
NOTE: AN UPDATE HAS BEEN POSTED AT WITH A NEW AND IMPROVED VERSION OF THIS SCRIPT.
1 thought on “New Event Report”
Comments are closed.