The other day someone who is learning PowerShell reached out to me with a problem. He couldn't understand why the relatively simple PowerShell expression to pull information from the System event log wasn't working. He wasn't seeing errors, but he also wasn't seeing the events he was expecting. Searching event logs with PowerShell is a common task. But as you'll see, you may need to update your approach to mining event logs with PowerShell. Things change in the PowerShell world, and sometimes in subtle ways that you may not notice. Although to be fair, some of these changes my arise from new versions of the .NET Framework and/or Windows 10. Here's what we encountered.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
Get-EventLog
From the very beginning we've used Get-EventLog to search classic event logs like System and Application. And that's what my student was doing as well in Windows PowerShell. He was searching the System event log for event id 1074 which indicates a computer restart. He was using code like this:
Get-EventLog -log system -newest 1000 |
Where-Object {$_.eventid -eq '1074'} |
Format-Table machinename, username, timegenerated -autosize
There's technically nothing wrong with this. I ran it and got 3 results. Then I double-checked the help to make sure I wasn't forgetting anything. That's when I saw the note indicating that Get-EventLog uses a deprecated Win32API. The note goes on to say that results may not be accurate. And since I know I've restarted my computer more than 3 times, this warning was right on. I occasionally still fire up a Get-EventLog command because the muscle memory is so strong. But now I know I really need to break this habit.
I knew that Get-EventLog isn't in PowerShell 7 and that you have to use Get-WinEvent. So I suggested going down that route.
Get-WinEvent
I'll be the first to admit that Get-WinEvent is a bit more complicated to learn, but it is also much more efficient. Here's an equivalent approach:
Get-WinEvent -filterhash @{Logname = 'system';ID=1074} -MaxEvents 1000 |
Format-Table Machinename,UserID,TimeCreated
When I run this I get 97 events which is considerably more accurate. The output from Get-WinEvent is different than Get-EventLog so you need to adjust property names. But filtering is much faster and easier. Now I can filter for the event ID early and not rely on Where-Object.
One critical difference for this particular task, is that we want to display the username. But Get-WinEvent reports a SID.
Fortunately, that property includes a method to translate the SID. Here's my revised code.
Get-WinEvent -filterhash @{Logname = 'system';ID=1074} -MaxEvents 1000 |
Select-Object @{Name="Computername";Expression = {$_.machinename}},
@{Name="UserName";Expression = {$_.UserId.translate([System.Security.Principal.NTAccount]).value}}, TimeCreated
The Translate() method may not always resolve the SID based. For example, I have credentials to query another laptop from my desktop, but I can't translate the SID other than the generic SYSTEM account. Fortunately, the replacement strings used in the event log record are stored under a "Properties" property.
The last item in this array is the user account. There's also some other useful information such as the type of restart event. With this in mind, I'll revise my code so that I can query remote machines.
Get-WinEvent -computer thinkp1 -filterhash @{Logname = 'system';ID=1074} -MaxEvents 1000 |
Select-Object @{Name="Computername";Expression = {$_.machinename}},
@{Name="UserName";Expression = { ($_.properties[-1]).value}}, TimeCreated,
@{Name="Category";Expression = {$_.properties[4].value}}
Much better.
Function Time
More than likely, the original code was something that would be run periodically. So instead of always having to type the code, creating a PowerShell function around it is the smart move. I already have Get-Winevent expression that works so that will be the center of my function. I always stress the importance of getting your core code to work at a console prompt first. Then build the function around it.
Because you want functions to be flexible, I thought a bit about what parameters I might need. Even though the original code was searching the local computer's event log, it isn't a stretch to imagine wanting to search a remote computer and Get-WinEvent supports that. As well as alternate credentials. I decided to keep the MaxEvents parameter. But I also imagined situations where I wanted to find restart events after a certain date.
Param(
[Parameter(Position = 0, ValueFromPipeline)]
[ValidateNotNullOrEmpty()]
[Alias("CN")]
[string]$Computername = $env:COMPUTERNAME,
[Parameter(HelpMessage = "Find restart events since this date and time.")]
[ValidateNotNullOrEmpty()]
[Alias("Since")]
[datetime]$After,
[int64]$MaxEvents,
[PSCredential]$Credential
)
You'll notice I kept the same parameter names. There's no reason to reinvent the wheel. Although I added a few parameter aliases. My Get-WinEvent command is going to use a filtering hashtable, so I'll build that on the fly.
$filter = @{
Logname = "System"
ID = 1074
}
if ($After) {
Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Getting restart events after $After"
$filter.Add("StartTime", $After)
}
The next step is to define a hashtable of Get-WinEvent parameter that I can splat. Splatting isn't always required or necessary but in this case it keeps my code simple.
$entries = Get-WinEvent @splat
Objects, Objects, Objects
You always want your functions to write objects to the pipeline. I could have used the native output from Get-WinEvent, but the original command only wanted a few properties so I'll do the same. I like creating custom objects like this:
foreach ($entry in $entries) {
#resolve the user SID
Try {
Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Translating $($entry.UserId)"
$user = $entry.UserId.translate([System.Security.Principal.NTAccount]).value
}
Catch {
$user = $entry.properties[-1].value
#$entry.userid
}
[pscustomobject]@{
PSTypeName = "RestartEvent"
Computername = $entry.machinename.ToUpper()
Datetime = $entry.TimeCreated
Username = $user
Category = $entry.properties[4].value
Process = $entry.properties[0].value.split()[0].trim()
}
} #foreach item
My function is using both techniques to resolve the user SID for the sake of education.
Polishing PowerShell
What I have to this point is useful. I've changed to using Get-WinEvent to search event logs and I've built a simple, reusable tool around it that I can use at a PowerShell prompt. But how about putting a high polish on this function? For example, even though the default output shows as a list, I know a table view would be easier to read. And how about a way to make different categories jump out?
You'll notice that my custom hashtable defines a typename. This is so that I can create a custom format ps1xml file using New-PSFormatXML. In the .ps1 file that defines the function I'll also load the format file.
Update-FormatData $PSScriptRoot\restartevent.format.ps1xml
In the format file, I'm going to group the output by computername. I'm also going to add some color coding using ANSI escape sequences.
You can grab the complete function and format file from Github. And even if you don't need the function, begin making the transition to using Get-WinEvent. It will be a little tricky at first, but it will be worth your time.
Hi Jeff,
Do you have any good idea how we can validate a user name and password combi is valid in powershell?
One big frustration when using powershell to set Windows Service or IIS pool credential is that you only know the error when the account is locked(unattended deployment). Because the error only emerges in eventviewer later on.
Thanks in advance.
The only way you can tell a credential is good is to use it. I suppose you could verify the username by comparing the credential name with a source. But you won’t know if the password is good until you use it.
Jeff, can we get restart-computer cmdlet a -reason parameter 😉 ? to put as the Reason for the Event Log Parsing…
You should post this as an issue at https://github.com/PowerShell/PowerShell/issues. Don’t expect to see this added to Windows PowerShell unless you write your own version of Restart-Computer to add this functionality.
Ok, thanks lot