Displaying array properties for custom PSObjects - powershell

I'm pretty new to PowerShell, so to learn the ropes better, I'm working on a Powershell function to return some basic overview information on computers in our network. I've gotten just about everything that I'm looking for, but I don't know how to display all results for arrays returned by the WMI queries for things like hard disks or MAC addresses.
For example, right now I'm using the WMI query "DHCPEnabled = TRUE" to detect active NICs and retrieve their MAC addresses - but on a laptop, it's theoretically possible that query could return both a wired and wireless NIC.
The output of this command would then display the custom PSObject that I create, but in the resultant PSObject, the property MACAddress will display blank. The results are there, and I could get to them via the pipeline or a Select-Object, but I don't know how to save them for a report or otherwise "prettify" them.
Here's the working function I have now, which makes an awful assumption that the first result returned is the only one I care about. Again, in this example, this is mostly a concern for hard disks and for MAC addresses, but I'd like to understand the concept behind it for future reference.
Function Get-PCInfo
{
[CmdletBinding()]
param(
[Parameter(Mandatory = $true,
Position = 0,
ValueFromPipeline = $true,
ValueFromPipelineByPropertyName = $true)]
[Alias("CName")]
[string[]] $ComputerName
)
foreach($cName in $ComputerName)
{
Write-Verbose "Testing connection to $cName"
If (Test-Connection -ComputerName $cName -BufferSize 16 -Quiet)
{
Write-Verbose "Connection successful."
Write-Verbose "Obtaining WMI objects from $cName"
$cs = Get-WMIObject -Class Win32_ComputerSystem -ComputerName $cName
$csp = Get-WMIObject -Class Win32_ComputerSystemProduct -ComputerName $cName
$os = Get-WMIObject -Class Win32_OperatingSystem -ComputerName $cName
$bios = Get-WMIObject -Class Win32_BIOS -ComputerName $cName
$cpu = Get-WMIObject -Class Win32_Processor -ComputerName $cName
$hdd = Get-WMIObject -Class Win32_LogicalDisk -Filter 'DeviceID = "C:"' -ComputerName $cName
$network = Get-WMIObject -Class Win32_NetworkAdapterConfiguration -Filter 'DHCPEnabled = True' -ComputerName $cName
if ($hdd -is [System.array])
{
Write-Verbose "Multiple hard drives detected; using first result"
$hddResult = $hdd[0]
} else {
Write-Verbose "Single hard drive detected"
$hddResult = $hdd
}
if ($network -is [System.array])
{
Write-Verbose "Multiple network cards detected; using first result"
$networkResult = $network[0]
} else {
Write-Verbose "Single network card detected"
$networkResult = $network
}
Write-Verbose "Creating output table"
$props = #{'Name' = $cs.Name;
'OSVersion' = $os.Version;
'ServicePack' = $os.ServicePackMajorVersion;
'HardDiskSize' = $hddResult.Size;
'SerialNumber' = $bios.serialNumber;
'Model' = $cs.Model;
'Manufacturer' = $cs.Manufacturer;
'Processor' = $cpu.Name;
'RAM' = $cs.TotalPhysicalMemory;
'MACAddress' = $networkResult.MACAddress}
Write-Verbose "Creating output object from table"
$result = New-Object -TypeName PSObject -Property $props
Write-Verbose "Outputting result"
$resultArray += #($result)
} else {
Write-Verbose "Connection failure"
$resultArray += #($null)
}
}
Write-Output $resultArray
}
Here's an example run, for some more clarity. The data is fake, but this is the format of the result:
PS> Get-PCInfo localhost
SerialNumber : 12345
MACAddress :
RAM : 4203204608
Manufacturer : Computers, Inc.
Processor : Intel(R) Core(TM) i5-2400 CPU # 3.10GHz
HardDiskSize : 500105736192
OSVersion : 6.2.9200
Name : PC1
Model: : Super Awesome Computer
ServicePack : 0
I'd like to send this to ConvertTo-HTML or something to make a nice-looking report, but because MACAddress is blank, I can't make anything nice out of it. What I'd like to see is something like this:
SerialNumber : 12345
MACAddress[0] : 00-11-22-33-44-55
MACAddress[1] : 88-99-AA-BB-CC-DD
...
HardDiskSize[0]: 500105736192
HardDiskSize[1]: 500105736192

I'm not quite sure I understand? It depends on how you want them to output. You can do it in many ways. An example for HDDs and MAC addresses:
....
'HardDiskSize' = ($hdd | % { "HDD $($_.DeviceID) - $($_.Size)" }) -join "`n"
....
'MACAddress' = ($networkResult | Select-Object -ExpandProperty MACAddress) -join "`n"
}
You can try this (untested). Copy and paste the edited parts back:
$hdd = #(Get-WMIObject -Class Win32_LogicalDisk -Filter 'DeviceID = "C:"' -ComputerName $cName)
$network = #(Get-WMIObject -Class Win32_NetworkAdapterConfiguration -Filter 'DHCPEnabled = True' -ComputerName $cName)
$props = #{'Name' = $cs.Name;
'OSVersion' = $os.Version;
'ServicePack' = $os.ServicePackMajorVersion;
'SerialNumber' = $bios.serialNumber;
'Model' = $cs.Model;
'Manufacturer' = $cs.Manufacturer;
'Processor' = $cpu.Name;
'RAM' = $cs.TotalPhysicalMemory;
Write-Verbose "Creating output object from table"
$result = New-Object -TypeName PSObject -Property $props
# Add MAC addresses
for ($i = 0; $i -lt $network.Count; $i++) {
Add-Member -InputObject $result -MemberType NoteProperty -Name "MACAddress[$i]" -Value $network[$i].MACAddress
}
# Add HDDs
for ($i = 0; $i -lt $hdd.Count; $i++) {
Add-Member -InputObject $result -MemberType NoteProperty -Name "HardDiskSize[$i]" -Value $hdd[$i].Size
}

Related

Powershell: How to incorporate if,elseif statements for adding columns into CSV file?

I'd like the following code to add the specified columns if it finds the appropriate graphics adapter in a pc.
Right now, my if/elseif statements are throwing all kinds of errors and I'm thinking its because I put it in the wrong section of the code. The columns are not being generated as how I would like for it to.
Any advice?
# User needs to create a txt file containing hostnames.
function ReadHostnames($initialDirectory) {
[void] [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
$OpenFileDialog = New-Object System.Windows.Forms.OpenFileDialog
if ($initialDirectory) { $OpenFileDialog.initialDirectory = $initialDirectory }
$OpenFileDialog.filter = 'All files (*.*)|*.*'
[void] $OpenFileDialog.ShowDialog()
return $OpenFileDialog.FileName
}
($FilePermissions = ReadHostnames C:\)
$FilePermissions = Get-Content $FilePermissions
write-host "Please wait while gathering information..."
$counter = 0
foreach ($computernames in $FilePermissions)
{
Write-host "Processing $computernames ($counter/$($FilePermissions.count))"
IF (Test-Connection -BufferSize 32 -Count 1 -ComputerName $computernames -Quiet)
{
$Computersystem = Get-WMIObject Win32_ComputerSystem -ComputerName $computernames -AsJob
$videocontroller = Get-WmiObject win32_videocontroller -ComputerName $computernames -AsJob
$bioscontroller1 = Get-WmiObject win32_bios -ComputerName $computernames -AsJob
$bioscontroller2 = Get-WmiObject -Class:Win32_ComputerSystem -ComputerName $computernames -AsJob
$userlogon = Get-CimInstance -ClassName Win32_ComputerSystem -Property UserName -ComputerName $computernames
Wait-Job -Job $Computersystem,$videocontroller,$bioscontroller -Timeout 10 | out-Null
$computersystem_output = Receive-Job $Computersystem
$intelvideocontroller_output = Receive-Job $videocontroller | ? {$_.name -ilike "*Intel*"}
$nvidiavideocontroller_output = Receive-Job $videocontroller | ? {$_.name -ilike "*NVIDIA*"}
$AMDvideocontroller_output = Receive-Job $videocontroller | ? {$_.name -ilike "*AMD*"}
$bioscontroller1_output = Receive-Job $bioscontroller1
$bioscontroller2_output = Receive-Job $bioscontroller2
# Creating spreadsheet headers
$newrow = [Pscustomobject] #{
Host_name = $computersystem_output.name
Model_Name = $bioscontroller2_output.Model
Serial_Number = $bioscontroller1_output.SerialNumber
BIOS_Version = $bioscontroller1_output.SMBIOSBIOSVersion
Last_User_Logon = $userlogon.UserName
If ($intelvideocontroller_output -ilike "*Intel*")
{ Intel_Card = $intelvideocontroller_output.name
IntelGraphics_Version = $intelvideocontroller_output.DriverVersion}
ElseIf ($nvidiavideocontroller_output -ilike "*NVIDIA*")
{ Nvidia_Card = $nvidiavideocontroller_output.name
NvidiaGraphics_Version = $nvidiavideocontroller_output.DriverVersion }
ElseIf ( $AMDvideocontroller_output -ilike "*AMD*")
{ AMD_Card = $AMDvideocontroller_output.name
AMDGraphics_Version = $AMDvideocontroller_output.DriverVersion }
}
$newrow | export-csv -path c:\HostnameData.csv -Append -NoTypeInformation
Remove-Job -job $Computersystem,$videocontroller,$bioscontroller1,$bioscontroller2 -Force
$counter++
}
Else
{
write-Host "The remote computer "$computernames" is Offline"
}
}
The logic would have to be wrapped inside some type of operator such as the grouping, or sub-expression operator to be allowed as the assignment of the name. A more concise solution is using a switch statement before your pscustomobject construct and having a dynamic assignment.
$card_type,
$card_version =
switch -Wildcard ($videocontroller.Name)
{
'*Intel*' { 'Intel_Card', 'IntelGraphics_Version' }
'*NVIDIA*' { 'Nvidia_Card', 'NvidiaGraphics_Version' }
'*AMD*' { 'AMD_Card', 'AMDGraphics_Version' }
$_ { 'N/A_Card', 'N/A_Version' }
}
$newrow = [pscustomobject]#{
Host_name = $computersystem_output.name
Model_Name = $bioscontroller2_output.Model
Serial_Number = $bioscontroller1_output.SerialNumber
BIOS_Version = $bioscontroller1_output.SMBIOSBIOSVersion
Last_User_Logon = $userlogon.UserName
$card_type = $videocontroller.Name
$card_version = $videocontroller.DriverVersion
}
Now the type of card gets saved to $card_type, and the driver version name gets saved to $card_version; it also accounts for cards that didn't meet that criteria and defaults to 'N/A'.
On another note, I personally don't see why you're querying the same class of Win32_ComputerSystem more than once while using jobs and having them wait. You are also using 2 different type of cmdlets that do the same thing when querying those classes. You should only need the first variable assignments and you'd only have to reference them once.
EDIT:
You also can't append properties with different names to a csv that has a column already being used by a different name.

Checking if windows security patches are installed on multiple servers

I am trying to check if the specified KB # that I have set in my variables list matches the full list of KB installed patches on the server. If it matches, it will display that the patch is installed, otherwise it will state that it is not installed.
The code below does not seem to work, as it is showing as not installed, but in fact it's already been installed.
[CmdletBinding()]
param ( [Parameter(Mandatory=$true)][string] $EnvRegion )
if ($EnvRegion -eq "kofax"){
[array]$Computers = "wprdkofx105",
"wprdkofx106",
"wprdkofx107",
$KBList = "KB4507448",
"KB4507457",
"KB4504418"
}
elseif ($EnvRegion -eq "citrix"){
[array]$Computers = "wprdctxw124",
"wprdctxw125",
$KBList = "KB4503276",
"KB4503290",
"KB4503259",
"KB4503308"
}
### Checks LastBootUpTime for each server
function uptime {
gwmi win32_operatingsystem | Select
#{LABEL='LastBootUpTime';EXPRESSION=
{$_.ConverttoDateTime($_.lastbootuptime)}} | ft -AutoSize
}
### Main script starts here. Loops through all servers to check if
### hotfixes have been installed and server last reboot time
foreach ($c in $Computers) {
Write-Host "Server $c" -ForegroundColor Cyan
### Checks KB Installed Patches for CSIRT to see if patches have been
### installed on each server
foreach ($elem in $KBList) {
$InstalledKBList = Get-Wmiobject -class Win32_QuickFixEngineering -
namespace "root\cimv2" | where-object{$_.HotFixID -eq $elem} |
select-object -Property HotFixID | Out-String
if ($InstalledKBList -match $elem) {
Write-Host "$elem is installed" -ForegroundColor Green
}
else {
Write-Host "$elem is not installed" -ForegroundColor Red
}
}
Write-Host "-------------------------------------------"
Invoke-Command -ComputerName $c -ScriptBlock ${Function:uptime}
}
Read-Host -Prompt "Press any key to exit..."
I would like to say that there is apparently a misconception about the ability to obtain information about all installed patches from Win32_QuickFixEngineering WMI class.
Even the official documentation states:
Updates supplied by Microsoft Windows Installer (MSI) or the Windows
update site (https://update.microsoft.com) are not returned by
Win32_QuickFixEngineering.
It seems that Win32_QuickFixEngineering is something like old fashioned approach which should be re replaced by using Windows Update Agent API to enumerate all updates installed using WUA - https://learn.microsoft.com/en-us/windows/win32/wua_sdk/using-the-windows-update-agent-api
Also, please take a loot at this good article - https://support.infrasightlabs.com/article/what-does-the-different-windows-update-patch-dates-stand-for/
You will find a lot of code examples by searching by "Microsoft.Update.Session" term
As Kostia already explained, the Win32_QuickFixEngineering does NOT retrieve all updates and patches. To get these, I would use a helper function that also gets the Windows Updates and returns them all as string array like below:
function Get-UpdateId {
[CmdletBinding()]
Param (
[string]$ComputerName = $env:COMPUTERNAME
)
# First get the Windows HotFix history as array of 'KB' id's
Write-Verbose "Retrieving Windows HotFix history on '$ComputerName'.."
$result = Get-HotFix -ComputerName $ComputerName | Select-Object -ExpandProperty HotFixID
# or use:
# $hotfix = Get-WmiobjectGet-WmiObject -Namespace 'root\cimv2' -Class Win32_QuickFixEngineering -ComputerName $ComputerName | Select-Object -ExpandProperty HotFixID
# Next get the Windows Update history
Write-Verbose "Retrieving Windows Update history on '$ComputerName'.."
if ($ComputerName -eq $env:COMPUTERNAME) {
# Local computer
$updateSession = New-Object -ComObject Microsoft.Update.Session
}
else {
# Remote computer (the last parameter $true enables exception being thrown if an error occurs while loading the type)
$updateSession = [activator]::CreateInstance([type]::GetTypeFromProgID("Microsoft.Update.Session", $ComputerName, $true))
}
$updateSearcher = $updateSession.CreateUpdateSearcher()
$historyCount = $updateSearcher.GetTotalHistoryCount()
if ($historyCount -gt 0) {
$result += ($updateSearcher.QueryHistory(0, $historyCount) | ForEach-Object { [regex]::match($_.Title,'(KB\d+)').Value })
}
# release the Microsoft.Update.Session COM object
try {
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($updateSession) | Out-Null
Remove-Variable updateSession
}
catch {}
# remove empty items from the combined $result array, uniquify and return the results
$result | Where-Object { $_ -match '\S' } | Sort-Object -Unique
}
Also, I would rewrite your uptime function to become:
function Get-LastBootTime {
[CmdletBinding()]
Param (
[string]$ComputerName = $env:COMPUTERNAME
)
try {
$os = Get-WmiObject -Class Win32_OperatingSystem -ComputerName $ComputerName
$os.ConvertToDateTime($os.LastBootupTime)
}
catch {
Write-Error $_.Exception.Message
}
}
Having both functions in place, you can do
$Computers | ForEach-Object {
$updates = Get-UpdateId -ComputerName $_ -Verbose
# Now check each KBid in your list to see if it is installed or not
foreach ($item in $KBList) {
[PSCustomObject] #{
'Computer' = $_
'LastBootupTime' = Get-LastBootTime -ComputerName $_
'UpdateID' = $item
'Installed' = if ($updates -contains $item) { 'Yes' } else { 'No' }
}
}
}
The output will be something like this:
Computer LastBootupTime UpdateID Installed
-------- -------------- -------- ---------
wprdkofx105 10-8-2019 6:40:54 KB4507448 Yes
wprdkofx105 10-8-2019 6:40:54 KB4507457 No
wprdkofx105 10-8-2019 6:40:54 KB4504418 No
wprdkofx106 23-1-2019 6:40:54 KB4507448 No
wprdkofx106 23-1-2019 6:40:54 KB4507457 Yes
wprdkofx106 23-1-2019 6:40:54 KB4504418 Yes
wprdkofx107 12-4-2019 6:40:54 KB4507448 No
wprdkofx107 12-4-2019 6:40:54 KB4507457 No
wprdkofx107 12-4-2019 6:40:54 KB4504418 Yes
Note: I'm on a Dutch machine, so the default date format shown here is 'dd-M-yyyy H:mm:ss'
Update
In order to alse be able to select on a date range, the code needs to be altered so the function Get-UpdateId returns an array of objects, rather than an array of strings like above.
function Get-UpdateId {
[CmdletBinding()]
Param (
[Parameter(Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true , Position = 0)]
[string]$ComputerName = $env:COMPUTERNAME
)
# First get the Windows HotFix history as array objects with 3 properties: 'Type', 'UpdateId' and 'InstalledOn'
Write-Verbose "Retrieving Windows HotFix history on '$ComputerName'.."
$result = Get-HotFix -ComputerName $ComputerName | Select-Object #{Name = 'Type'; Expression = {'HotFix'}},
#{Name = 'UpdateId'; Expression = { $_.HotFixID }},
InstalledOn
# or use:
# $result = Get-WmiobjectGet-WmiObject -Namespace 'root\cimv2' -Class Win32_QuickFixEngineering -ComputerName $ComputerName |
# Select-Object #{Name = 'Type'; Expression = {'HotFix'}},
# #{Name = 'UpdateId'; Expression = { $_.HotFixID }},
# InstalledOn
# Next get the Windows Update history
Write-Verbose "Retrieving Windows Update history on '$ComputerName'.."
if ($ComputerName -eq $env:COMPUTERNAME) {
# Local computer
$updateSession = New-Object -ComObject Microsoft.Update.Session
}
else {
# Remote computer (the last parameter $true enables exception being thrown if an error occurs while loading the type)
$updateSession = [activator]::CreateInstance([type]::GetTypeFromProgID("Microsoft.Update.Session", $ComputerName, $true))
}
$updateSearcher = $updateSession.CreateUpdateSearcher()
$historyCount = $updateSearcher.GetTotalHistoryCount()
if ($historyCount -gt 0) {
$result += ($updateSearcher.QueryHistory(0, $historyCount) | ForEach-Object {
[PsCustomObject]#{
'Type' = 'Windows Update'
'UpdateId' = [regex]::match($_.Title,'(KB\d+)').Value
'InstalledOn' = ([DateTime]($_.Date)).ToLocalTime()
}
})
}
# release the Microsoft.Update.Session COM object
try {
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($updateSession) | Out-Null
Remove-Variable updateSession
}
catch {}
# remove empty items from the combined $result array and return the results
$result | Where-Object { $_.UpdateId -match '\S' }
}
The Get-LastBootTime function does not need changing, so I leave you to copy that from the first part of the answer.
To check for installed updates by their UpdateId property
$Computers | ForEach-Object {
$updates = Get-UpdateId -ComputerName $_ -Verbose
$updateIds = $updates | Select-Object -ExpandProperty UpdateId
# Now check each KBid in your list to see if it is installed or not
foreach ($item in $KBList) {
$update = $updates | Where-Object { $_.UpdateID -eq $item }
[PSCustomObject] #{
'Computer' = $_
'LastBootupTime' = Get-LastBootTime -ComputerName $_
'Type' = $update.Type
'UpdateID' = $item
'IsInstalled' = if ($updateIds -contains $item) { 'Yes' } else { 'No' }
'InstalledOn' = $update.InstalledOn
}
}
}
Output (something like)
Computer : wprdkofx105
LastBootupTime : 10-8-2019 20:01:47
Type : Windows Update
UpdateID : KB4507448
IsInstalled : Yes
InstalledOn : 12-6-2019 6:10:11
Computer : wprdkofx105
LastBootupTime : 10-8-2019 20:01:47
Type :
UpdateID : KB4507457
IsInstalled : No
InstalledOn :
To get hotfixes and updates installed within a start and end date
$StartDate = (Get-Date).AddDays(-14)
$EndDate = Get-Date
foreach ($computer in $Computers) {
Get-UpdateId -ComputerName $computer |
Where-Object { $_.InstalledOn -ge $StartDate -and $_.InstalledOn -le $EndDate } |
Select-Object #{Name = 'Computer'; Expression = {$computer}},
#{Name = 'LastBootupTime'; Expression = {Get-LastBootTime -ComputerName $computer}}, *
}
Output (something like)
Computer : wprdkofx105
LastBootupTime : 20-8-2019 20:01:47
Type : HotFix
UpdateId : KB4474419
InstalledOn : 14-8-2019 0:00:00
Computer : wprdkofx107
LastBootupTime : 20-8-2019 20:01:47
Type : Windows Update
UpdateId : KB2310138
InstalledOn : 8-8-2019 15:39:00

Options to use different param

I'm looking for a way to to have a choice of a list or a single computername in a foreach loop.
If the user enters in a single computername I want the script to execute for that one computername
but if that user wants to use a path to a list of computers how could I replace $computername with the path that user wants?
function Get-OSInfo {
[CmdletBinding()]
param (
#[Parameter(ValueFromPipeline=$True,
# ValueFromPipelineByPropertyName=$True)]
[string]$computername,
[string]$errorlog = 'c:\errors.txt',
[switch]$logerrors
)
PROCESS {
foreach ($computer in $computername) {
Try {
$os = Get-WmiObject -EA Stop –Class Win32_OperatingSystem –ComputerName $computer
$cs = Get-WmiObject -EA Stop –Class Win32_ComputerSystem –ComputerName $computer
$bios = Get-WmiObject -EA Stop –Class Win32_BIOS –ComputerName $computer
$cpu = Get-WmiObject -EA Stop -class Win32_processor -ComputerName $computer
$props = #{'ComputerName'=$computer;
'OSVersion'=$os.version;
'SPVersion'=$os.servicepackmajorversion;
'OSBuild'=$os.buildnumber;
'OSArchitecture'=$os.osarchitecture;
'Manufacturer'=$cs.manufacturer;
'Model'=$cs.model;
'BIOSSerial'=$bios.serialnumber
'CPU Count'=$CPU.Count
'Memory'= [Math]::round(($cs.TotalPhysicalMemory/1gb),2)
'CPU Speed'= $CPU.MaxClockSpeed[0]}
$obj = New-Object -TypeName PSOBject -Property $props
$obj.PSObject.TypeNames.Insert(0,'Get-OS.OSInfo')
#Write-Output $obj
$obj | Export-Csv c:\test4.csv -Append
} Catch {
if ($logerrors) {
$computer | Out-File $errorlog -append
}
Write-Warning "$computer failed"
}
}
}
}
Change the type of the $ComputerName parameter to a string array instead of just a single string:
param(
[string[]]$ComputerName,
[string]$errorlog = 'c:\errors.txt',
[switch]$logerrors
)
Notice the [] after the type name, this denotes an array of strings, rather than a single string.
Now you can do:
PS C:\> $computers = Get-Content C:\computers.txt
PS C:\> Get-OSInfo -ComputerName $computers
If you'd like to be able to specify a path to a file containing the target computers as the argument to the function, you can use multiple parameter sets:
[CmdletBinding(DefaultParameterSetName='ByName')]
param(
[Parameter(ParameterSetName='ByName',ValueFromPipeline)]
[string[]]$ComputerName,
[Parameter(ParameterSetName='ByFile')]
[string]$InputFile
)
begin {
if($PSCmdlet.ParameterSetName -eq 'ByFile'){
try{
$ComputerName = Get-Content -LiteralPath $InputFile
}
catch{
throw
return
}
}
}
process {
foreach($Computer in $ComputerName){
# Work with $Computer here...
}
}

get-process product version remote computer

If I locally do this I get all the information:
get-process | select-object name,fileversion,company
However, if I do it on a remote computer I only get the process name and all the other fields are blank. Does anyone know why or how to get the same information. I am using a domain admin credential so I should have access to that information.
get-process -computername xcomp123 | select-object name,fileversion,company
You can try this solution:
$Computername = 'Remotehost'
$Session = New-CimSession -ComputerName $Computername
$process = Get-CimInstance -ClassName Win32_Process -CimSession $Session
$col = New-Object System.Collections.ArrayList
foreach ($n in $process){
$exePath = $null
$ExeInfo = $null
$exePath = $n.ExecutablePath -Replace '\\','\\'
$ExeInfo = Get-CimInstance -ClassName Cim_DataFile -Filter "Name = '$exePath'" -ErrorAction SilentlyContinue
[void]$col.add([PSCustomObject]#{
Name = $n.name
FileVersion = $ExeInfo.Version
Company = $ExeInfo.Manufacturer
PSComputername = $n.PSComputername
})
}
Remove-Cimsession $session
$col
Update:
I reduced the code to check for one process only. I assert the referencefile having the same name, as the process on the client computers. You might change that for your needs.
You can specify multiple computers at $computername, so you do not have to run the code over and over again.
#region Reference file
$RefFile = Get-item "\\x123\c$\program files\prog\winagent\file.exe"
#endregion
#region remote file
[string[]]$Computername = 'Remotehost1', 'Remotehost2'
$Processname = $RefFile.Name
foreach ($n in $Computername) {
$Session = New-CimSession -ComputerName $n
$process = Get-CimInstance -ClassName Win32_Process -CimSession $Session -Filter "name = '$Processname'"
$exePath = $process.ExecutablePath -Replace '\\', '\\'
$ExeInfo = Get-CimInstance -ClassName Cim_DataFile -Filter "Name = '$exePath'" -ErrorAction SilentlyContinue
[PSCustomObject]#{
Name = $Processname
FileVersion = $ExeInfo.Version
Company = $ExeInfo.Manufacturer
PSComputername = $n
}
Remove-Cimsession $session
}
#endregion

PowerShell Printer port property hostaddress not always populated

We're trying to create a list of all the printers on a print server with their respective HostAddress for the shared port they use. To do this we created the following function:
Function Get-PrintersInstalledHC {
Param (
[Parameter(ValueFromPipeline)]
[Object[]]$Servers
)
Process {
foreach ($S in $Servers) {
Try {
if ($Printers = Get-Printer -ComputerName $S.Name -Full -EA Stop) {
$CimParams = #{
ClassName = 'Win32_PrinterConfiguration'
ComputerName = $S.Name
Property = '*'
ErrorAction = 'Stop'
}
$Details = Get-CimInstance #CimParams
$Ports = Get-CimInstance -ClassName Win32_TCPIPPrinterPort -ComputerName $S.Name -Property *
Foreach ($P in $Printers) {
Foreach($D in $Details) {
if ($P.Name -eq $D.Name) {
$Prop = #{
PortHostAddress = $Ports | Where {$_.Name -eq $P.PortName} |
Select -ExpandProperty HostAddress
DriverVersion = $D.DriverVersion
Collate = $D.Collate
Color = $D.Color
Copies = $D.Copies
Duplex = $D.Duplex
PaperSize = $D.PaperSize
Orientation = $D.Orientation
PrintQuality = $D.PrintQuality
MediaType = $D.MediaType
DitherType = $D.DitherType
RetrievalDate = (Get-Date -Format 'dd/MM/yyyy HH:mm')
}
$P | Add-Member -NotePropertyMembers $Prop -TypeName NoteProperty
Break
}
}
}
[PSCustomObject]#{
ComputerName = $S.Name
ComputerStatus = 'Ok'
RetrievalDate = (Get-Date -Format 'dd/MM/yyyy HH:mm')
Printers = $Printers
}
}
}
Catch {
if (Test-Connection $S.Name -Count 2 -EA Ignore) {
[PSCustomObject]#{
ComputerName = $S.Name
ComputerStatus = "ERROR: $($Error[0].Exception.Message)"
RetrievalDate = (Get-Date -Format 'dd/MM/yyyy HH:mm')
Printers = $null
}
}
else {
[PSCustomObject]#{
ComputerName = $S.Name
ComputerStatus = 'Offline'
RetrievalDate = (Get-Date -Format 'dd/MM/yyyy HH:mm')
Printers = $null
}
}
}
}
}
}
This function works fine in a mixed environment and gives us the full list of all the printers installed on a server with their properties. However, the property HostAddress (renamed to PortHostAddress in the function above) is not always populated.
This is also illustrated with the following code, as not all printers are in the output:
Get-WmiObject Win32_Printer -ComputerName $PrintServer | ForEach-Object {
$Printer = $_.Name
$Port = $_.PortName
Get-WmiObject Win32_TCPIpPrinterPort -ComputerName $PrintServer | where {$_.Name -eq $Port} |
select #{Name="PrinterName";Expression={$Printer}}, HostAddress
}
For 90% of all printers the HostAddress can be found with this code. But sometimes it can't be found and the field stays empty because there is no match between the Name and the PortName.
Is there a better way of retrieving this property that works a 100% of the time?
Since the additional data states the problem ports are using drivers different from Microsoft's TCP/IP printer port driver, parsing these ports' addresses would require interacting with the drivers, this is dependant on the driver in question. So skip it, or convert a remote port to Microsoft's "Standard TCP/IP port" if possible. HP printers are easily converted, WSD printers can be converted by creating a TCP/IP port with the IP address of a WSD printer and assigning a static IP address on that printer, and about the same procedure could work with "Advanced TCP/IP port"s. The ports that are labeled "Local" ports are software-based, and you can use the host's IP address in place of missed PortHostAddress.