Skip to content
Menu
The Lonely Administrator
  • PowerShell Tips & Tricks
  • Books & Training
  • Essential PowerShell Learning Resources
  • Privacy Policy
  • About Me
The Lonely Administrator

Answering the WSMan PowerShell Challenge

Posted on September 30, 2020September 30, 2020

Today, I thought I'd share my solution to a recent Iron Scripter challenge. I thought this was a fascinating task and one with a practical result.  I'd encourage you to try your hand at the challenge and come back later to see how I tackled it.

Manage and Report Active Directory, Exchange and Microsoft 365 with
ManageEngine ADManager Plus - Download Free Trial

Exclusive offer on ADManager Plus for US and UK regions. Claim now!

The challenge is not only a good test of your PowerShell scripting skills but also a test of how well you understand PowerShell remoting.  In the long run, I have no doubt that PowerShell remoting will mean SSH. But that isn't coming anytime soon. Traditional PowerShell remoting using WSMan is still the way most IT Pros manage remote servers. This means you should understand it and be able to manage it.

The scripting challenge was to create a tool that would query a remote server for WSMan connections and write a custom object to the pipeline with specific pieces of information. Here's how I approached this.

Know Your Processes

The first thing to know is that when a PowerShell remoting connection is established, it runs in a wsmprovhost process. Querying for this process is simple.

wsmprovhostI could also have used Get-CimInstance. A useful feature of Get-Process is the ability to get the process owner, or username, for each process. However, this only works when Get-Process is run on the remote host. No problem. I can use Invoke-Command.

wsmprovhost-owner I'm filtering out the process ID of the wsmprovhost process that is temporarily created by Invoke-Command. The process is transient in this situation, but if I was using an existing PSSession to see who else was connected, I want to filter it out. That was one of the challenge criteria as well.

The process object can provide a number of pieces of information the challenge was looking for. Although, one of the requirements was to get associated child processes. I found using Get-CimInstance to be useful for this task.

childprocess

Get-WSManInstance

The other parts of the challenge require a bit more effort.  When a PowerShell remoting connection is established, there is a corresponding WSMan instance. As you may know, when you connect via PowerShell remoting, you are connecting to a session endpoint. The Get-PSSessionConfiguration cmdlet displays these endpoints. Almost always, these endpoints initiate a PowerShell session on the remote computer. In WSMan-speak, this runs in a shell.

PowerShell has a cmdlet called Get-WSManInstance that allows you to enumerate these connections. wsmaninstanceAs you can see, there is some useful information. One of the challenge tasks was to get the shell runtime. That is listed here but because this result is essentially an XML document, it isn't in a friendly format.

Fortunately, there is an XML method that will convert these values.

convert-lexical

Get-PSRemoteSessionUser

Those where the tricky bits. Tasks like getting the source hostname from the IP address is easy with Resolve-DNSName. With all of this in mind, I wrote a PowerShell function called Get-PSRemoteSessionUser.

Function Get-PSRemoteSessionUser {
    [cmdletbinding(DefaultParameterSetName = "ComputerName")]
    Param(
        [Parameter(ParameterSetName = 'Session', Position = 0)]
        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.Runspaces.PSSession[]]$Session,

        [Parameter(ParameterSetName = 'ComputerName', Mandatory, Position = 0)]
        [Alias('Cn')]
        [ValidateNotNullOrEmpty()]
        [string[]]$ComputerName,

        [Parameter(ParameterSetName = 'ComputerName', ValueFromPipelineByPropertyName)]
        [pscredential]$Credential,

        [Parameter(ParameterSetName = 'ComputerName')]
        [System.Management.Automation.Runspaces.AuthenticationMechanism]$Authentication,

        [Parameter(ParameterSetName = 'ComputerName')]
        [Parameter(ParameterSetName = 'Session')]
        [int]$ThrottleLimit
    )

    Begin {
        Write-Verbose "[BEGIN  ] Starting: $($MyInvocation.Mycommand)"
        #define the scriptblock to run remotely

        $run = {
            $VerbosePreference = $using:VerbosePreference
            Write-Verbose "[$([environment]::machinename)] Querying for local WSMan sessions"
            $process = 'wsmprovhost'

            #these helper functions will be used to get and format connection data
            Function _enumWsMan {
                [cmdletbinding()]
                Param()

                Get-WSManInstance -ResourceURI Shell -Enumerate |
                    Select-Object -Property Name, State, ShellID, Owner, ClientIP, ProcessID,
                    @{Name = "Memory"; Expression = { _parseMemoryString $_.memoryUsed } },
                    @{Name = "ShellRunTime"; Expression = { [System.Xml.XmlConvert]::ToTimeSpan($_.ShellRunTime) } },
                    @{Name = "ShellInactivity"; Expression = { [System.Xml.XmlConvert]::ToTimeSpan($_.ShellInactivity) } },
                    @{Name = "MaxIdleTimeout"; Expression = { [System.Xml.XmlConvert]::ToTimeSpan($_.MaxIdleTimeout) } },
                    @{Name = "SessionConfiguration"; Expression = { Split-Path -path $_.resourceuri -leaf } }

            }
            Function _parseMemoryString {
                #convert values like 11MB to 11534336
                [cmdletbinding()]
                Param([string]$StringValue)

                switch -Regex ($StringValue ) {
                    "\d+KB" {
                        $val = 1KB
                    }
                    "\d+MB" {
                        $val = 1MB
                    }
                    "\d+GB" {
                        $val = 1GB
                    }

                } #switch
                if ($val) {
                    [int]$i = ([regex]"\d+").Match($StringValue).value
                    $i * $val
                }
                else {
                    Write-Warning "Failed to parse $StringValue"
                    $stringValue
                }

            } #close function

            try {
                Write-Verbose "[$([environment]::machinename)] Getting $process process excluding id $PID"
                $p = (Get-Process -Name $process -IncludeUserName -erroraction stop).where({ $_.id -ne $pid })
            }
            Catch [Microsoft.PowerShell.Commands.ProcessCommandException] {
                Write-Warning "Could not find process $process on this computer."
            }
            Catch {
                Throw $_
            }

            if ($p) {

                foreach ($item in $p) {

                    if ($item.username) {
                        Write-Verbose "[$([environment]::machinename)] Getting the SID for $($item.username)"
                        $SID = [System.Security.Principal.NTAccount]::new("$($item.username)").Translate([System.Security.Principal.SecurityIdentifier]).value
                    }
                    else {
                        $SID = $null
                    }
                    #call a private function to enumerate WSMan connection associated with this process ID
                    Write-Verbose "[$([environment]::machinename)] Enumerating WSMan connections"
                    $connData = $(_enumWsMan).where({ $_.processid -eq $item.ID })

                    #get child process IDs
                    Write-Verbose "[$([environment]::machinename)] Getting child processes for id $($item.id)"
                    $childProcs = (Get-CimInstance -ClassName win32_process -filter "ParentProcessId = $($item.id)" -Property ProcessID).ProcessID

                    #resolve the hostname
                    #temporarily disable Verbose to eliminate verbose messages from loading the DNSClient module
                    $VerbosePreference = "SilentlyContinue"
                    Import-Module DNSClient
                    $VerbosePreference = $using:VerbosePreference
                    Try {
                        Write-Verbose "[$([environment]::machinename)] Resolving the hostname for $($conndata.ClientIP)"
                        $rHost = (Resolve-DnsName -Name $connData.ClientIP -ErrorAction Stop).NameHost
                    }
                    Catch {
                        Write-Verbose "[$([environment]::machinename)] Failed to resolve a hostname for $($connData.ClientIP)."
                        $rHost = $connData.clientIP
                    }
                     Write-Verbose "[$([environment]::machinename)] Returning connection data"
                    #Send data back to the host to construct a custom object
                    @{
                        rHost        = $rHost
                        Item         = $item
                        SID          = $SID
                        Computername = [environment]::MachineName
                        ChildProcs   = $childProcs
                        ConnData     = $connData
                    }

                } #foreach item
            }
            else {
                Write-Verbose "[$([environment]::machinename)] No running $process process(e$as) found"
            }
        } #close scriptblock

        $PSBoundParameters.add("Scriptblock", $Run)
        $PSBoundParameters.Add("HideComputername", $True)
    } #begin

    Process {
        Write-Verbose "[PROCESS] Getting remote connection data"
        $data = Invoke-Command @PSBoundParameters
        foreach ($result in $data) {
            Write-Verbose "[PROCESS] Processing data for $($result.computername)"
            [pscustomobject]@{
                PSTypename           = "PSRemoteSessionUser"
                Computername         = $result.Computername
                DateUTC              = (Get-Date).ToUniversalTime()
                StartTimeUTC         = $result.item.StartTime.ToUniversalTime()
                ProcessID            = $result.item.id
                ChildProcesses       = $result.childProcs
                Username             = $result.item.Username
                SID                  = $result.sid
                State                = $result.connData.State
                RemoteHost           = $result.rHost
                RemoteIPAddress      = $result.connData.ClientIP
                ShellRunTime         = $result.conndata.ShellRunTime
                SessionConfiguration = $result.connData.SessionConfiguration
                Memory               = $result.connData.Memory
            }
        } #foreach result
    } #process

    End {
        Write-Verbose "[END    ] Ending: $($MyInvocation.Mycommand)"
    } #end

} #close Get-RemoteSessionUser

The function is designed to let me query remote computers by name or through an existing session. If I use an existing session, it will be filtered out from the results. Most of the core querying happens remotely. The "raw" data is returned to the local host from Invoke-Command and turned into a custom object.

My function uses verbose output and even passes my verbose preference to the remote computer.

get-psremotesessionuser-1What did I get back?

get-psremotesessionuser-2The custom object has properties that should meet all of the challenge requirements.

Custom Formatting

But there is one more thing. The last part of the challenge was to create a custom formatting file. The results I have a great, but maybe I typically only need to see a formatted subset of properties. Using New-PSFormatXML from the PSScriptTools module, I created a format.ps1xml. After a bit of tweaking in VS Code, I can load it into my session.

Update-FormatData psremotesessionuser.format.ps1xml

And now get nicely formatted results.

get-psremotesessionuser-3

Here's the format file.

<?xml version="1.0" encoding="UTF-8"?>
<!--
format type data generated 09/15/2020 08:54:32 by BOVINE320\Jeff
using New-PSFormatXML from the PSScriptTools module.

Install-Module PSScriptTools
-->
<Configuration>
  <ViewDefinitions>
    <View>
      <!--Created 09/15/2020 08:54:32 by BOVINE320\Jeff-->
      <Name>default</Name>
      <ViewSelectedBy>
        <TypeName>PSRemoteSessionUser</TypeName>
      </ViewSelectedBy>
      <GroupBy>
        <PropertyName>Computername</PropertyName>
      </GroupBy>
      <TableControl>
        <!--Delete the AutoSize node if you want to use the defined widths.-->
        <AutoSize />
        <TableHeaders>
          <TableColumnHeader>
            <Label>SessionConfiguration</Label>
            <Width>23</Width>
            <Alignment>left</Alignment>
          </TableColumnHeader>
          <TableColumnHeader>
            <Label>Address</Label>
            <Width>14</Width>
            <Alignment>left</Alignment>
          </TableColumnHeader>
          <TableColumnHeader>
            <Label>Username</Label>
            <Width>15</Width>
            <Alignment>left</Alignment>
          </TableColumnHeader>
          <TableColumnHeader>
            <Label>State</Label>
            <Width>12</Width>
            <Alignment>left</Alignment>
          </TableColumnHeader>
          <TableColumnHeader>
            <Label>Runtime</Label>
            <Width>15</Width>
            <Alignment>left</Alignment>
          </TableColumnHeader>
          <TableColumnHeader>
            <Label>MemMB</Label>
            <Width>12</Width>
            <Alignment>right</Alignment>
          </TableColumnHeader>
          <TableColumnHeader>
            <Label>PID</Label>
            <Width>12</Width>
            <Alignment>right</Alignment>
          </TableColumnHeader>
        </TableHeaders>
        <TableRowEntries>
          <TableRowEntry>
            <TableColumnItems>
              <TableColumnItem>
                <PropertyName>SessionConfiguration</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>RemoteIPAddress</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>Username</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>State</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>ShellRunTime</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <ScriptBlock>$_.Memory/.1MB -as [int]</ScriptBlock>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>ProcessID</PropertyName>
              </TableColumnItem>
            </TableColumnItems>
          </TableRowEntry>
        </TableRowEntries>
      </TableControl>
    </View>
  </ViewDefinitions>
</Configuration>

Limitations

I now have a PowerShell tool I can run to see who has connections to servers and the state of those sessions.

get-psremotesessionuser-4Remember, this is only showing remote connections via WSMan. If someone is connected with SSH, or PowerShell Direct (to a virtual machine), I won't "see" them. But in my situation, those would be special use edge cases anyway.

Next Steps

I hope you tried your hand at the challenge and will keep an eye on the Iron Scripter site for future challenges. They are a great way to test your knowledge and expand your skills. If you are looking to learn a bit more about PowerShell, you might take a look at my PowerShell Remoting Fundamentals course from Pluralsight. Or visit my Books and Training page.

I hope you found this informative and useful. Comments and feedback are always welcome.


Behind the PowerShell Pipeline

Share this:

  • Click to share on X (Opens in new window) X
  • Click to share on Facebook (Opens in new window) Facebook
  • Click to share on Mastodon (Opens in new window) Mastodon
  • Click to share on LinkedIn (Opens in new window) LinkedIn
  • Click to share on Pocket (Opens in new window) Pocket
  • Click to share on Reddit (Opens in new window) Reddit
  • Click to print (Opens in new window) Print
  • Click to email a link to a friend (Opens in new window) Email

Like this:

Like Loading...

Related

1 thought on “Answering the WSMan PowerShell Challenge”

  1. Pingback: ICYMI: PowerShell Week of 02-October-2020 | PowerShell.org

Comments are closed.

reports

Powered by Buttondown.

Join me on Mastodon

The PowerShell Practice Primer
Learn PowerShell in a Month of Lunches Fourth edition


Get More PowerShell Books

Other Online Content

github



PluralSightAuthor

Active Directory ADSI Automation Backup Books CIM CLI conferences console Friday Fun FridayFun Function functions Get-WMIObject GitHub hashtable HTML Hyper-V Iron Scripter ISE Measure-Object module modules MrRoboto new-object objects Out-Gridview Pipeline PowerShell PowerShell ISE Profile prompt Registry Regular Expressions remoting SAPIEN ScriptBlock Scripting Techmentor Training VBScript WMI WPF Write-Host xml

©2025 The Lonely Administrator | Powered by SuperbThemes!
%d