- Install Ansible on Windows - Thu, Jul 20 2023
- Use Azure Bastion as a jump host for RDP and SSH - Tue, Apr 18 2023
- Azure Virtual Desktop: Getting started - Fri, Apr 14 2023
Windows PowerShell eventing
Eventing has been a part of the language since version 2.0. Although, in my opinion, it is more of a “true blue” developer’s topic, we sysadmins have every right to know how to subscribe to and receive event messages from .NET Framework objects through the PowerShell runtime. Let’s get started.
Understanding eventing from a high level
According to the Microsoft Developer Network (MSDN) website, an event is “a message sent by an object to signal the occurrence of an action.” Well, we know that all data contained in a PowerShell pipeline are objects, right? It makes sense that these objects can transmit status messages on their own behalf; this is “eventing.”
Here’s the high-level workflow for how Windows PowerShell eventing works:
- You discover the object event or events that you’re interested in monitoring.
- You subscribe to the event using the appropriate Register- cmdlet.
- You periodically check the event queue programmatically and format your output as appropriate.
- You purge events from the queue when you’re finished.
I’ll spend the rest of our time together today walking you through each of the previous steps, one at a time.
To give this tutorial maximum real-world relevance to you, the Windows systems administrator, let’s say that we suspect one of our Windows Server 2012 R2 file servers may be compromised with malware. We’ll use Windows Management Instrumentation (WMI) temporary event subscriptions to record all process start and stop events.
Yes, you heard me correctly: we’ll create a temporary event subscription here, but you can configure permanent event subscriptions if you want to. You’ll find that PowerShell experts even created CLI and graphical front ends for permanent event subscriptions!
Discovering your chosen object event
You can see which, if any, events your favorite Windows PowerShell/.NET objects exposed by pipelining the object into Get-Member:
Get-Process -Name notepad | Get-Member -MemberType Event TypeName: System.Diagnostics.Process Name MemberType Definition ---- ---------- ---------- Disposed Event System.EventHandler Disposed(System.Object, Sy... ErrorDataReceived Event System.Diagnostics.DataReceivedEventHandler Er... Exited Event System.EventHandler Exited(System.Object, Syst... OutputDataReceived Event System.Diagnostics.DataReceivedEventHandler Ou...
Of the four built-in process events, the only one that makes sense to me immediately is Exited, which fires when the process exits from its runspace. That makes sense.
I’ve found that the object events that PowerShell natively exposes don’t give us the specific data we’re after.
As I said earlier, we want Windows PowerShell to fire events when new processes are started. You and I both know that the WMI/Common Information Model (CIM) repository stores this information. We simply need to apply some research and a good WMI Query Language (WQL) query to fetch our results. Try this for process starts:
SELECT * FROM Win32_ProcessStartTrace
And this for process terminations:
SELECT * FROM Win32_ProcessStopTrace
By the way, I’m adapting this Process Watcher example from the work of Windows PowerShell MVPs Boe Prox and Richard Siddaway.
Subscribing to .NET events in Windows PowerShell
We can create subscriptions to three different event types, and the Windows PowerShell team gave us a cmdlet for each type:
- Register-ObjectEvent: Subscribe to .NET object events.
- Register-EngineEvent: Subscribe to PowerShell runtime events.
- Register-WmiEvent: Subscribe to Windows Management Instrumentation (WMI) object events.
The following event subscription looks a bit gnarly at first glance. However, stick with me and you’ll be fine; I’ll explain what’s going on line by line:
$hindex = '"{0}","{1}","{2}","{3}","{4}"' $header = $hindex -f 'Host', 'Process ID', 'Process Name', 'Timestamp', 'Source ID' Add-Content -Path 'C:\process-start.txt' -Value $header $pstart = 'SELECT * FROM Win32_ProcessStartTrace' $pstop = 'SELECT * FROM Win32_ProcessStopTrace' $action = { $nevent = $Event.SourceEventArgs.NewEvent $hindex = '"{0}","{1}","{2}","{3}","{4}"' $data = $hindex -f $Event.Sender.Scope.Path.Server, $nevent.ProcessID, $nevent.ProcessName, $Event.TimeGenerated, $Event.SourceIdentifier Add-Content -Path 'C:\process-start.txt' -Value $data } Register-WmiEvent -Query $pstart -SourceIdentifier 'Process.Start' -Action $action Register-WmiEvent -Query $pstop -SourceIdentifier 'Process.Stop' -Action $action Get-Content -Path "c:\process-start.txt"
1-2: All we’re doing here is applying some PowerShell composite string formatting to set up our log file. The $hindex variable defines five index positions, and the $header variable links the index positions to five friendly column names with the -f operator. You can use that {} composite string formatting syntax to adjust column widths and alignment and even apply styles such as currency formatting.
4: Here, we create a new log file to store our captured events and specify the $header contents as our first row. After all, what’s a log file without column headings?
6-7: These variables simply store our WQL queries: one for process starts and the other for process stops. We could just write the queries longhand in our Register-WmiEvent statements, but we can make our registrations more compact by creating separate variables.
9-14: We need to create an action that PowerShell takes when a process start or stop event is detected. Our action taps deeply into the $Event automatic variable and pulls out useful properties that (not coincidentally) align with the column headings we defined in the $header variable earlier in the script.
The $Event automatic variable stores metadata about (you guessed it) events that PowerShell/.NET objects generate.
10: The $nevent variable is simply a shortcut way to refer to the $Event.SourceEventArgs.NewEvent object.
11: Once again, we are going to use composite string formatting to make five-column output.
12: The $data variable holds the actual properties that we want to see in our process-start.txt log file.
13: Here, we use Add-Content to put the event metadata into the log file.
15-16: As I stated earlier, we make our Register-WmiEvent statements easier to read because we’ve parameterized the -Query and -Action values.
18: This is simply a shortcut to view the log file contents on the PowerShell console.
Checking the event queue
We can list our current session’s event subscriptions with the Get-EventSubscriber cmdlet:
Get-EventSubscriber SubscriptionId : 1 SourceObject : System.Management.ManagementEventWatcher EventName : EventArrived SourceIdentifier : Process.Start Action : System.Management.Automation.PSEventJob HandlerDelegate : SupportEvent : False ForwardEvent : False SubscriptionId : 2 SourceObject : System.Management.ManagementEventWatcher EventName : EventArrived SourceIdentifier : Process.Stop Action : System.Management.Automation.PSEventJob HandlerDelegate : SupportEvent : False ForwardEvent : False
As you can see in the Action property, PowerShell considers these temporary event subscriptions to be honest-to-goodness background jobs (run Get-Job to prove this to yourself).
To fire example process start events, start Notepad and Calc from Windows. After they’re open, close them to fire process stop events.
Let’s have a look at that logfile:
Get-Content -Path "C:\process-start.txt" "Host","ID","Name","Time","Source" "Host","Process ID","Process Name","Timestamp","Source ID" "localhost","3316","calc.exe","4/30/2015 9:36:47 AM","Process.Start" "localhost","4064","notepad.exe","4/30/2015 9:36:51 AM","Process.Start" "localhost","3316","calc.exe","4/30/2015 9:36:54 AM","Process.Stop" "localhost","4064","notepad.exe","4/30/2015 9:36:55 AM","Process.Stop"
Now we’re cooking with gas! If our file server is indeed infected with malware, we should be able to track the rogue process starts and stops and isolate the problem in no time.
What’s even cooler is that, thanks to Windows PowerShell remoting, we can subscribe to events generated on remote computers as well.
Removing subscriptions and purging the queue
Remember that these event subscriptions persist only for the current Windows PowerShell session. Therefore, closing your console window automagically makes your event subscriptions go “bye bye.”
We can remove individual events:
Get-Event –SourceIdentifier | Remove-Event
Or we can unregister entire event subscriptions:
Unregister-Event –SourceIdentifier Process.Start Unregister-Event –SourceIdentifier Process.Stop
Lastly, we can purge the entire subscription event queue by leveraging the Windows PowerShell pipeline:
Get-EventSubscriber | Unregister-Event -Force
Conclusions
Remember that what we did in this article is create temporary event subscriptions. This means that our subscriptions will monitor and potentially report on subscribed events only for as long as your console session is active. Alternatively, of course, you can unregister your subscription from the current session.
An important note: if you try this exercise in Windows PowerShell v5 preview, you’ll get unexpected results. Specifically, I couldn’t get my subscription to track process start events at all. Remember that PowerShell v5 is not fully “baked,” so make sure you experiment with this feature in Windows PowerShell v4 or earlier.
Note to self: I should write 4sysops blog posts on (a) creating permanent event subscriptions, and (b) using composite string formatting to get precisely the tabular output you need. Thanks a lot for reading, and take care.
“I should write 4sysops blog posts on (a) creating permanent event subscriptions”
Please do! I Created a script that remaps a printer on rdp reconnect, and I would really like to turn it into a permanent event.