Solving the Remoting Scripting Challenge
In this issue:
At the end of last month I left you with what I think is an interesting PowerShell scripting challenge. If you followed along with what I published this month, hopefully you picked up some tips and code that you could use. Let's see what you came up with. As always, my solutions are not the solutions. And they may not be the best solution for your environment. The point of these challenges is to learn. Let's see what you learned.
For the challenge I wanted you to monitor a remote computer and capture information when someone creates a PowerShell remoting session to it. I wanted you to capture when the session begins and how long it lasts.
You should capture the following information:
- The process ID
- The start time
- The end time
- the process/session run time
- The user's SID. Bonus points if you can resolve it to a user name in the domain\username format.
- The computer name
For advanced scripters, do the same thing but watch for SSH PowerShell remoting connections to the computer. I left it to you to decide what to do with the information.
WSMan Monitoring
When a remote connection is made to a computer using PowerShell remoting, the PowerShell process is managed by a new process, Wsmprovhost.exe
. This process is created when the connection is made and it is destroyed when the connection is closed. The process owner should be the user that made the connection.
PS C:\> Get-Process wsmprovhost -IncludeUserName
Handles WS(K) CPU(s) Id UserName ProcessName
------- ----- ------ -- -------- -----------
692 84464 0.27 1952 COMPANY\AprilS wsmprovhost
The solution is to create an event subscription that watches for the creation of this process. When it is created, capture the information you need. I demonstrated a few ways to accomplish this over the last few weeks.
I want to be able to monitor connections remotely. I'll create the event subscriptions on a Windows client with remote queries to a domain-joined server. I originally thought I could create CIM indication subscriptions to watch for the process creation and removal.
$computer = 'SRV1'
$startQuery = "Select * from __InstanceCreationEvent within 2 WHERE TargetInstance ISA 'Win32_process' AND TargetInstance.Name = 'wsmprovhost.exe'"
$stopQuery = "Select * from __InstanceDeletionEvent within 2 WHERE TargetInstance ISA 'Win32_process' AND TargetInstance.Name = 'wsmprovhost.exe'"
These queries allow me to capture more information, but I couldn't easily pass the CIM instance to Invoke-CimMethod
to get the process owner. I also found that this subscription took too long to process and missed shorter connections such as when using Invoke-Command
.
Using ProcessTrace Classes
Instead, I opted to use the Win32_ProcessStartTrace
and Win32_ProcessStopTrace
event classes. These classes provide a subset of properties and run faster from what I can tell. I'll setup an event subscription to watch for new Wsmprovhost.exe
processes.
$computername = "SRV1"
$startQuery = "Select * from Win32_ProcessStartTrace within 2 Where ProcessName = 'wsmprovhost.exe'"
Register-CimIndicationEvent -Query $startQuery -ComputerName $Computername -SourceIdentifier WSManStart
When a remote connection is made, the event subscription triggers and an event is created.
PS C:\> Get-Event -SourceIdentifier WSManStart
ComputerName :
RunspaceId : c73ef194-7c66-41c3-b43e-3d791d194cf7
EventIdentifier : 1
Sender : Microsoft.Management.Infrastructure.CimCmdlets.CimIndicationWatcher
SourceEventArgs : Microsoft.Management.Infrastructure.CimCmdlets.CimIndicationEventInstanceEventArgs
SourceArgs : {Microsoft.Management.Infrastructure.CimCmdlets.CimIndicationWatcher, }
SourceIdentifier : WSManStart
TimeGenerated : 9/25/2025 8:25:02 AM
MessageData :
PS C:\> (Get-Event -id 1).SourceEventArgs
NewEvent MachineId Bookmark Context
-------- --------- -------- -------
Win32_ProcessStartTrace MI_SUBSCRIBE_BOOKMARK_NEWEST
PS C:\> (Get-Event -id 1).SourceEventArgs.NewEvent
ParentProcessID : 692
ProcessID : 1952
ProcessName : wsmprovhost.exe
SECURITY_DESCRIPTOR :
SessionID : 0
Sid : {1, 5, 0, 0...}
TIME_CREATED : 134032875020878486
PSComputerName :
Since I want to know how long the session lasts, I also need to monitor when the process stops. I'll create a second event subscription for that. I thought it would be as simple as this.
$stopQuery = "Select * from Win32_ProcessStopTrace within 2 Where ProcessName = 'wsmprovhost.exe'"
However, this event was not triggered when I closed the remote session and I couldn't figure out why. Eventually, I tested with a wildcard pattern.
$stopQuery = "Select * from Win32_ProcessStopTrace within 2 Where ProcessName LIKE 'wsm%'"
For some reason that I don't understand, even though the starting process name is wsmprovhost.exe
, when the process stops, the name is truncated wsmprovhost.ex
. I have to assume this is a bug because I have never seen this with other processes.
Once I had this worked out, I revised by notification query.
$stopQuery = "Select * from Win32_ProcessStopTrace within 2 Where ProcessName = 'wsmprovhost.ex'"
When the process starts, I want to capture information about the remote session and log it to a CSV file. This means I need to define a script block to run when the event is triggered. Remember, the script block is executing on the client computer using information from the triggered event.
In the script block, the process ID of the stopped process should be the same as the started process. I'll search the event queue for the matching start event so that I can get the start time. I need that to calculate the run time.
To get the process owner, I have the SID stored as a byte array. I demonstrated a few techniques this month to convert the SID to a user name. In my script block I'm going to convert the byte array to string value.
$sidValue = (New-Object System.Security.Principal.SecurityIdentifier($instance.sid, 0)).Value
I can then find the corresponding user account using Get-CimInstance
and the Win32_UserAccount
class.
$owner = Get-CimInstance -ClassName win32_UserAccount -Filter "Sid='$sidValue'" -ComputerName $CN
Resolving SIDs
Here's the tricky part. If the logged on remote user is using a local account, this command will work. However, if the user is using a domain account, it will fail because the local computer doesn't have that account and wants to query the domain controller. This requires a 2nd hop which isn't allowed by default using WSMan. Remember, you have to keep in mind where your command is running. The script block is running on the client computer connecting to the remote computer using Get-CimInstance
. Under the hood this is using the WSMan protocol. If the remote computer needs to make an authenticated 2nd hop to another network resource, it won't work. That's what is happening in this situation. Again, if the SID belongs to a local member account, the command will work.
If it fails, I'll try the query again but this time locally on the client computer. Assuming it is a domain user, the client computer should be able to resolve the domain.
Finally, I'll create a custom object with the information I want and export it to a CSV file. The file name will include the computer name so that if I monitor multiple computers, I can keep the logs separate.
Here is the rest of the monitoring code.
$LogPath = "C:\temp"
$action = {
$instance = $event.SourceEventArgs.NewEvent
#extract the computer name from the CIM system properties
$CN = $instance.CimSystemProperties.ServerName.ToUpper()
#find matching start event
$start = (Get-Event -SourceIdentifier WSManStart ).where({ $_.SourceEventArgs.NewEvent.ProcessID -eq $instance.ProcessID })
if ($Start) {
#convert the SID byte array to a string value
$sidValue = (New-Object System.Security.Principal.SecurityIdentifier($instance.sid, 0)).Value
#This will fail if the SID belongs to a domain account
$owner = Get-CimInstance -ClassName win32_UserAccount -Filter "Sid='$sidValue'" -ComputerName $CN
if (-Not $owner.caption) {
#try again using the local computer which is assumed to be a domain member
$owner = Get-CimInstance -ClassName win32_UserAccount -Filter "Sid='$sidValue'"
}
$processStart = $start.TimeGenerated
$run = New-TimeSpan -Start $processStart -End $event.TimeGenerated
}
else {
$owner = $Null
$run = $Null
$processStart = $Null
}
$log = Join-Path -path $LogPath -ChildPath "$CN-remote-log.csv"
[PSCustomObject]@{
ProcessName = $instance.ProcessName
ProcessID = $instance.ProcessID
Start = $processStart
End = $event.TimeGenerated
Runtime = $run
User = $owner.Caption
Computername = $CN
} | Export-Csv -Path $log -Append
}
Register-CimIndicationEvent -Query $stopQuery -ComputerName $computername -SourceIdentifier WSManStop -Action $action