Behind the PowerShell Pipeline logo

Behind the PowerShell Pipeline

Subscribe
Archives
September 26, 2025

Solving the Remoting Scripting Challenge

In this issue:

  • WSMan Monitoring
    • Using ProcessTrace Classes
    • Resolving SIDs
  • PowerShell over SSH
    • Creating a Dynamic Event Subscription
  • Summary

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
Want to read the full issue?
GitHub Bluesky LinkedIn Mastodon https://jdhitsoluti…
Powered by Buttondown, the easiest way to start and grow your newsletter.