Yesterday I shared a script that you could use to inventory systems for Windows PowerShell and PowerShell 7 installations. This should work for most people who install PowerShell 7 with the provided installer. But, as has been pointed out more than once to me, this won't detect any side-loaded or out-of-band installations. I made reference to this in the previous article. I think the best you can do is search the hard drive instances of pwsh.exe. But we don't want to do a brute-force recursive directory search. Instead, I'm going to use Get-CimInstance.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
Get-CimInstance -ClassName CIM_Logicalfile -Filter "filename='pwsh' AND extension = 'exe' AND drive='C:'"
I'm limiting search to drive C to speed things up. This type of search finds both stable and preview installations.
This gives me the same information I had before plus a bit more. I can still search for Windows PowerShell the same way using Get-Command.
The Script
Here's a revised script.
#requires -version 5.1
[cmdletbinding()]
Param(
[Parameter(HelpMessage = "Specify the drive to search for instances of pwsh.exe")]
[ValidatePattern("^[c-zC-Z]$")]
[string]$Drive = "C"
)
#a private function to shorten filenames
Function _shortName {
Param($path)
$parts = $path.split("\")
$out = foreach ($part in $parts) {
if ($part -match "\s") {
"{0}~1" -f $part.Substring(0, 6)
}
else {
$part
}
}
$out -join "\"
}
Write-Verbose "Searching for PowerShell installations on $([System.Environment]::MachineName)"
$os = (Get-CimInstance -ClassName Win32_OperatingSystem -Property Caption).caption
#region Windows Powershell
Try {
Write-Verbose "Testing for Windows PowerShell"
$cmd = Get-Command -Name powershell.exe -ErrorAction stop
if ($cmd) {
Write-Verbose "Using $($cmd.path)"
#need to run Powershell.exe to get $PSVersion
$psh = &$cmd.path -nologo -noprofile -command { Get-Host }
#test for PowerShell 2.0 engine or feature
Write-Verbose "Testing for Windows PowerShell 2.0 engine or feature"
#get computersystem roles to determine if running on a server or client
#assuming operating system caption uses 'Server'
if ($os.caption -match "server") {
Write-Verbose "Running Get-WindowsFeature"
$f = Get-WindowsFeature PowerShell-V2
}
else {
Write-Verbose "Running Get-WindowsOptionalFeature"
$f = Get-WindowsOptionalFeature -Online -FeatureName MicrosoftWindowsPowerShellV2Root
}
if ($f.installed -OR $f.State -eq 'Enabled') {
$comment = "Windows PowerShell 2.0 feature enabled or installed"
}
else {
$comment = ""
}
#the install date will reflect the last time the OS was updated or installed
[pscustomobject]@{
PSTypeName = "PSInstallInfo"
Name = $cmd.Name
Path = $cmd.source
PSVersion = $psh.Version
Installed = (Get-Item $cmd.source).CreationTime
Comments = $comment
Computername = [System.Environment]::MachineName
OperatingSystem = $os
}
Remove-Variable cmd
} #$cmd found
} #try
Catch {
#this should never happen
Write-Verbose "Windows PowerShell not installed on $([Environment]::MachineName)."
}
#endregion
#region PowerShell 7
Write-Verbose "Testing for PowerShell 7 including preview builds"
$pwsh = Get-CimInstance -ClassName CIM_Logicalfile -Filter "filename='pwsh' AND extension = 'exe' AND drive='$($drive):'"
if ($pwsh) {
Foreach ($item in $pwsh) {
if ($item.path -match 'preview') {
$comment = "Preview"
}
else {
$comment = ""
}
#test for SSH
if (Test-Path $env:programdata\ssh\sshd_config ) {
#get short name
$short = _shortName $item.EightDotThreeFileName
Write-Verbose "Testing for an SSH subsystem using $short"
$ssh = Get-Content $env:programdata\ssh\sshd_config | Select-String $($short -replace "\\", "\\")
#'(?<=powershell\s).*pwsh.exe'
if ($ssh.matches.value -eq $short) {
$comment += " SSH configured"
}
}
[pscustomobject]@{
PSTypeName = "PSInstallInfo"
Name = Split-Path $item.name -Leaf
Path = Split-Path $item.name
PSVersion = $item.version
Installed = $item.InstallDate
Comments = $comment.Trim()
Computername = [System.Environment]::MachineName
OperatingSystem = $os
}
}
}
#endregion
#end of script
Changes
I made a few changes in this version. First, I decided not to treat the PowerShell 2.0 engine as a separate version. You use the same powershell.exe version for both 5.1 and 2.0. Instead, I reflect the 2.0 engine status in the comment. I also took a different approach to SSH. Instead of testing if SSH is installed and running, I'm checking the sshd_config file for the existence of a PowerShell subsystem. This is what lets your connect using SSH remoting in PowerShell.
This requires a little manipulation because the long file name from Windows is truncated in the sshd_config file. So instead of searching for C:\Program Files\PowerShell\7\pwsh.exe", I'm looking for "c:\progra~1\powershell\7\pwsh.exe". I wrote a short helper function to convert the path. I realize this is not foolproof but it works for my SSH installations.
Here's default local output.
As before, you can use remoting to inventory other servers and desktops.
$c = Invoke-Command -FilePath C:\scripts\Get-PSExeFile.ps1 -ComputerName win10,srv1,srv2 -HideComputerName | sort computername
Do you like that? I revised the format ps1xml file.
<!--
Format type data generated 07/13/2021 12:14:45 by PROSPERO\Jeff
This file was created using the New-PSFormatXML command that is part
of the PSScriptTools module.
https://github.com/jdhitsolutions/PSScriptTools
-->
<Configuration>
<ViewDefinitions>
<View>
<!--Created 07/13/2021 12:14:45 by PROSPERO\Jeff-->
<Name>default</Name>
<ViewSelectedBy>
<TypeName>PSInstallInfo</TypeName>
</ViewSelectedBy>
<GroupBy>
<ScriptBlock>
if ($host.name -match "console|code|serverremotehost") {
"{0} [$([char]27)[3m{1}$([char]27)[0m]" -f $_.Computername,$_.OperatingSystem
}
else {
"{0} [{1}]" -f $_.Computername,$_.OperatingSystem
}
</ScriptBlock>
<Label>Computername</Label>
</GroupBy>
<TableControl>
<!--Delete the AutoSize node if you want to use the defined widths
<AutoSize />.-->
<TableHeaders>
<TableColumnHeader>
<Label>Name</Label>
<Width>19</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>PSVersion</Label>
<Width>16</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Installed</Label>
<Width>12</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Comments</Label>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>Name</PropertyName>
</TableColumnItem>
<TableColumnItem>
<ScriptBlock>
if (($host.name -match "console|code|serverremotehost") -AND ($_.path -match "preview")) {
"$([char]27)[38;5;219m$($_.PSVersion)$([char]27)[0m"
}
else {
$_.PSVersion
}
</ScriptBlock>
</TableColumnItem>
<TableColumnItem>
<ScriptBlock>$_.Installed.ToShortDateString()</ScriptBlock>
</TableColumnItem>
<TableColumnItem>
<ScriptBlock>
if (($host.name -match "console|code|serverremotehost") -AND ($_.comments-match "SSH configured")) {
<!-- replace SSH Detected with an ANSI sequence-->
$_.comments -replace "SSH configured","$([char]27)[1;38;5;155mSSH configured$([char]27)[0m"
}
else {
$_.comments
}
</ScriptBlock>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
</ViewDefinitions>
</Configuration>
Summary
At this point, you should have several options. Hopefully one of them meets your needs. Or feel free to cut and paste the parts you want into your own tool. If you take this approach, I hope you'll share your work. Questions and comments always welcome. Enjoy.
Hi Jeffrey,
it’s always a pleasure to get inspired by your blog posts. I learn something everytime =)
Today I found a little irritation:
‘Get-WindowsOptionalFeature -Online’ needs elevated rights to get executed successfully.
If this is not given, it triggeres the ‘Catch’ block, even with ‘-ErrorAction SilentlyContinue’ added.
So it seems to be a hard error.
But the problem is, as non-admin user I still find Windows PowerShell before, but got knocked out by searching for the Windows PowerShell 2.0 feature.
Do you have an idea to circumvent the need of ‘Get-WindowsOptionalFeature’ to find, if Windows PowerShell 2.0 feature is installed?
Or at least give the PSO with information of Windows PowerShell found and handle the search of Windows PowerShell 2.0 feature somewhere else ;D
Best Regards
Not off the top of my head. I will have to look at this.
Get-WindowsOptionalFeature must be run elevated. I suppose I should add #requires runasadministrator to the strip. You could check for the existence of the sshd service. Or check the registry: get-childitem ‘hklm:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\CapabilityIndex\OpenSSH*’
Regarding to
https://serverfault.com/questions/406808/how-can-we-check-powershell-v2-0-is-installed-on-a-list-of-servers
it would be possible to check the registry:
# WPS versions = 3
$RegPath = “SOFTWARE\Microsoft\PowerShell\3\PowerShellEngine”
$ValueName = “PowerShellVersion”
That is true. But as has been pointed out to me on several occasions, that may not reflect all instances especially if something was setup without going through an MSI install.