A wrapper for netstat and thoughts on general text-parsing with PS

A while ago, I had to use netstat while investigating an issue on an endpoint system, and got to wondering about whether PowerShell had a replacement for it. There isn’t a single equivalent, although there are two cmdlets, Get-NetTCPConnection and Get-NetUDPEndpoint, which perform more or less the same function. I got to thinking that it would be useful to have an object-based version that would allow for easy interaction and manipulation of the output.

For context, here are a couple of sceenshots showing the query I’m using and the expected output for TCP and UDP connections:
Output for the command netstat -a -f -n -o, showing active connections. The results are formatted in a table with columns for protocol, local address, foreign address, state and PID. In this iage, only TCP connections are visible.
A second set of output for the command netstat -a -f -n -o, showing active connections. The results are formatted in a table with columns for protocol, local address, foreign address, state and PID. This time UDP connections are shown.

It didn’t take too long to write the wrapper below for netstat:

Function Get-Netstat {
	$netstat = cmd /c "netstat -a -f -n -o"
	$connections=@()
	foreach ($line in $netstat) {
		if ($line -match "^  TCP|^  UDP") {
			# Create new object
			$Obj = New-Object PSObject

			# Protocol
			Add-Member -InputObject $Obj -MemberType NoteProperty -Name Protocol -Value ($line -split " " | ? {$_ -ne ""})[0]

			# Source IP & Port. Can be IPv4 or v6
			if (($line -split " " | ? {$_ -ne ""})[1] -match "\[") {
				# IPv6
				Add-Member -InputObject $Obj -MemberType NoteProperty -Name Type -Value "IPv6"
				Add-Member -InputObject $Obj -MemberType NoteProperty -Name SourceIP -Value $((($line -split " " | ? {$_ -ne ""})[1] -split "]:")[0]+"]")
				Add-Member -InputObject $Obj -MemberType NoteProperty -Name SourcePort -Value (($line -split " " | ? {$_ -ne ""})[1] -split "]:")[1]
			} else {
				# IPv4
				Add-Member -InputObject $Obj -MemberType NoteProperty -Name Type -Value "IPv4"
				Add-Member -InputObject $Obj -MemberType NoteProperty -Name SourceIP -Value (($line -split " " | ? {$_ -ne ""})[1] -split ":")[0]
				Add-Member -InputObject $Obj -MemberType NoteProperty -Name SourcePort -Value (($line -split " " | ? {$_ -ne ""})[1] -split ":")[1]
			}
			# Destination IP & Port. Can be IPv4 or v6
			if (($line -split " " | ? {$_ -ne ""})[2] -match "\[") {
				# IPv6
				Add-Member -InputObject $Obj -MemberType NoteProperty -Name DestIP -Value $((($line -split " " | ? {$_ -ne ""})[2] -split "]:")[0]+"]")
				Add-Member -InputObject $Obj -MemberType NoteProperty -Name DestPort -Value (($line -split " " | ? {$_ -ne ""})[2] -split "]:")[1]
			} else {
				# IPv4
				Add-Member -InputObject $Obj -MemberType NoteProperty -Name DestIP -Value (($line -split " " | ? {$_ -ne ""})[2] -split ":")[0]
				Add-Member -InputObject $Obj -MemberType NoteProperty -Name DestPort -Value (($line -split " " | ? {$_ -ne ""})[2] -split ":")[1]
			}
			
			# Connection state - only relevant for TCP connections so check protocol property
			if ($Obj.Protocol -eq "TCP") {
				Add-Member -InputObject $Obj -MemberType NoteProperty -Name State -Value ($line -split " " | ? {$_ -ne ""})[3]
			} else {
				Add-Member -InputObject $Obj -MemberType NoteProperty -Name State -Value $null
			}
			
			# PID 
			if ($Obj.Protocol -eq "TCP") {
				Add-Member -InputObject $Obj -MemberType NoteProperty -Name PID -Value ($line -split " " | ? {$_ -ne ""})[4]
			} else {
				Add-Member -InputObject $Obj -MemberType NoteProperty -Name PID -Value ($line -split " " | ? {$_ -ne ""})[3]
			}
			
			# Additional process information
			try {
				$procinfo=get-Process -PID ($obj.PID) -ErrorAction Stop
				Add-Member -InputObject $Obj -MemberType NoteProperty -Name ImageName -Value $($procinfo.Name)
				Add-Member -InputObject $Obj -MemberType NoteProperty -Name SessionId -Value $($procinfo.SessionId)
			} catch {
				Add-Member -InputObject $Obj -MemberType NoteProperty -Name ImageName -Value "Unknown"
				Add-Member -InputObject $Obj -MemberType NoteProperty -Name SessionId -Value "Unknown"
			}

			# Add to connections array, remove variables
			$connections+=$Obj
			Remove-Variable -Name Obj,procinfo -Force -ErrorAction SilentlyContinue
		}
	}
	Remove-variable -name netstat -force -erroraction silentlycontinue
	return $connections
}

The function does 3 things:

  1. invoke netstat with the desired switches, and pipe the output into a variable $netstat
  2. iterate through each line of $netstat, processing lines with a protocol type near the start and assigning properties to a custom object, which is then added to the $connections array
  3. when complete, return the $connections array

I’m happy with it in its current form so I’ve put it on Github. While a line-by line parse of netstat’s output is not a particularly efficient approach, in practical terms it works as I need it to and there aren’t any noticeable performance issues. But I thought it would be interesting to see what other approaches people have taken to this.

The first one I saw was this one, a broadly similar idea – the text parsing is simpler because it is geared around returning strings rather than objects, but I like the alternate approach for getting the process ID – it’s probably faster as it’s a single call to Get-Process and subsequent searches through an array rather than my script’s per-iteration call to get-process, so that’s a refinement I could make. On a similar note, there’s also a thread on reddit about a similar function with some other suggestions about assembling a hashtable for creating the custom object rather than repeatedly adding properties individually, and using arraylists rather than arrays.

However, there were two posts that really got me thinking more – this one from the Scripting Guy and this one from Adam The Automator. The Scripting Guy’s post impressed me because, while I’m not a big fan of one-liners (due to the impact this has on their readability for others, particularly those less familiar with syntax and outputs), being able to get the same effect as my function in one line is pretty impressive.

This is all down to using ConvertFrom-String, a cmdlet introduced in PS5 that is also mentioned (but not used) by Adam The Automator in the post I linked to. This has the potential to be a very useful cmdlet. ConvertFrom-String is a generalised tool for taking string input and attempting to split it into a useful set of properties that can be treated as a custom object, though it is limited by a requirement for the input to be formatted consistently (so for example, to parse the output of netstat with ConvertFrom-String, you need to select the lines that are parsed so as to skip the column headers).

It appears to be the generalised complement to more specific cmdlets like ConvertFrom-JSON. I spent a little while looking at this recently in my own time. I’ve been helping a friend get up to speed with PowerShell, as they’re looking to automate some currently-manual processes in their environment. Said friend is proficient in bash, but has been struggling a bit with the object-oriented nature of powershell.

One process that they need to automate is querying Amazon’s Route 53 DNS service. They had an initial workflow built around string-chopping in bash to get the hosted zone ids, iterating through the hosted zone ids to get each one’s resource record sets which were all dumped into one file, and subsequently doing a fair bit of text parsing to extract the name and DNSName for each resource in the resource record set. As an initial effort, it worked, but was quite cumbersome.

I mentioned ConvertFrom-JSON as I thought this might simplify things, but on initial testing this did not work. I couldn’t see an obvious reason for this, which got me interested. We later figured out that the problem was most likely syntax-related, specifically that my friend had simply written all the JSON documents sequentially into a single text file, but I was happy for the excuse to poke at something new and unfamiliar.

I decided that it would be best to work out the required process for getting the information out of a single record, to reduce the number of moving parts. I started by looking at some of Amazon’s documentation for route 53, including these pages. This gave me a better understanding of the general structure of the responses I would expect, but did not include example values that I could use for testing. However, this question on StackOverflow included an example response that looked suitable for initial testing:

{
    "NextToken": "eyJTdGFydFJlY29yZE5hbWUiOiBudWxsLCAiU3RhcnRSZWNvcmRJZGVudGlmaWVyIjogbnVsbCwgIlN0YXJ0UmVjb3JkVHlwZSI6IG51bGwsICJib3RvX3RydW5jYXRlX2Ftb3VudCI6IDV9",
    "ResourceRecordSets": [
        {
            "ResourceRecords": [
                {
                    "Value": "ns-1264.awsdns-30.org."
                },
                {
                    "Value": "ns-698.awsdns-23.net."
                },
                {
                    "Value": "ns-1798.awsdns-32.co.uk."
                },
                {
                    "Value": "ns-421.awsdns-52.com."
                }
            ],
            "Type": "NS",
            "Name": "mydomain.com.",
            "TTL": 300
        },
        {
            "ResourceRecords": [
                {
                    "Value": "ns-1264.awsdns-30.org. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400"
                }
            ],
            "Type": "SOA",
            "Name": "mydomain.com.",
            "TTL": 300
        },
        {
            "ResourceRecords": [
                {
                    "Value": "12.23.34.45"
                }
            ],
            "Type": "A",
            "Name": "abcdefg.mydomain.com.",
            "TTL": 300
        },
        {
            "ResourceRecords": [
                {
                    "Value": "34.45.56.67"
                }
            ],
            "Type": "A",
            "Name": "zyxwvut.mydomain.com.",
            "TTL": 300
        },
        {
            "ResourceRecords": [
                {
                    "Value": "45.56.67.78"
                }
            ],
            "Type": "A",
            "Name": "abcdxyz.mydomain.com.",
            "TTL": 300
        }
    ]
}

With that sample value saved in a text file, I was able to confirm that “Get-Content -Raw” is all you need to read a JSON string into PowerShell so that it can be piped through to ConvertFrom-JSON. At this point the PSCustomObject has been created, so all that is needed is working out which properties in the object hold the required information. In this case, I used a CustomProperty to extract the Value of the resource record, with an if-statement to filter out SoA and NS records:

foreach ($set in $PSObject.ResourceRecordSets) {
    if ($set.Type -match "^A") {
        $set | Select -Property Name,@{Name='RecordValue';Expression={$_.ResourceRecords.Value}}
    }
}

Here’s another screenshot:
A PowerShell terminal in which a .json file has been read into a variable, then piped through ConvertFrom-JSON.

With this working, I was satisfied that this would likely fit easily into my friend’s current approach easily enough, but I again decided to have a look at other options for doing this. I very quickly happened across the AWSPowerShell package, and in particular the command Get-R53ResourceRecordSet.

Having written a number of scripts to parse text input into something more useful or meaningful in the past, I generally feel that it is a last-resort approach, and I feel that this post has been a useful demonstration of that. It’s almost always worth doing a bit of reading to see if there is a better way to do what you’re trying to do or if there’s a pre-built module that already implements the custom functions you need.

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.