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.
Related
I am attempting to build a script that will request information (Hostname, MAC, IP, Caption (os version), and serial number using a list of computers pulled from AD.
This works but it creates multiple lines/rows when instead I need all this information on one row.
Yes I am a noob at this.. I can write a script for a single machine just fine but getting that same script to work with a list remotely eludes me, this script allows me to get the information but not on the same row.!!
I am using PW version 5.1
Here it is;
Function Get-CInfo {
$ComputerName = Get-Content C:\Users\scott.hoffman.w.tsc\Desktop\scripts\get-cinfo-tools\comp-list.txt
$ErrorActionPreference = 'Stop'
foreach ($Computer in $ComputerName) {
Try {
gwmi -class "Win32_NetworkAdapterConfiguration" -cn $Computer | ? IpEnabled -EQ "True" | select DNSHostName, MACAddress, IPaddress | FT -AutoSize
gwmi win32_operatingsystem -cn $computer | select Caption | FT -Autosize
Get-WmiObject win32_bios -cn $computer | select Serialnumber | FT -Autosize
}
Catch {
Write-Warning "Can't Touch This : $Computer"
}
}#End of Loop
}#End of the Function
Get-CInfo > comp-details.txt
the comp-list.txt file is just;
computername01
computername02
I would love to use csv from input to output but I get lost.
Thanks for your help/input/kick in the pants!
Do yourself a huge favour and learn how to create custom objects:
# Function is more useful if you remove specific filepaths from inside it
# Using a parameter and set it to accept pipeline input
Function Get-CInfo {
[CmdletBinding()]
Param (
[parameter(Mandatory = $true,ValueFromPipeline = $true)]$ComputerList
)
Begin {
$ErrorActionPreference = 'Stop'
Write-Host "Processing:"
}
Process {
foreach ($Computer in $ComputerList) {
Try {
Write-Host $Computer
# Gather data
$NetAdapter = gwmi -class "Win32_NetworkAdapterConfiguration" -cn $Computer | ? IpEnabled -EQ "True" | select DNSHostName, MACAddress, IPaddress
$OStype = gwmi win32_operatingsystem -cn $computer | select Caption
$Serial = Get-WmiObject win32_bios -cn $computer | select Serialnumber
# Output custom object with required properties
[pscustomobject]#{
Computer = $Computer
DNSHostName = $NetAdapter.DNSHostName;
MACAddress = $NetAdapter.MACAddress;
IPAddress = $NetAdapter.IPAddress;
OperatingSystem = $OSType.Caption;
Serial = $Serial.Serialnumber;
Error = ''
}
}
Catch {
# Within the catch section $_ always contains the error.
[pscustomobject]#{
Computer = $Computer
DNSHostName = '';
MACAddress = '';
IPAddress = '';
OperatingSystem = '';
Serial = '';
Error = $_.Exception.Message
}
}
}#End of Loop
}
End {
Write-Host "Done"
}
}#End of the Function
# Pipe list to function and store to '$Results'
$Results = Get-Content C:\Users\scott.hoffman.w.tsc\Desktop\scripts\get-cinfo-tools\comp-list.txt | Get-CInfo
# Output and formatting should almost always be the last thing you do
# Now that you have an object ($Results) with your data, you can use it however you like
# Format and output to text file
$Results | ft -AutoSize > comp-details.txt
# Or send to csv
$Results | Export-Csv -Path comp-details.csv -NoTypeInformation
Thanks to #Scepticalist!
Here is the PS script with which I read a text file line by line into a variable:
text
ComputerName01
ComputerName02
Script
function Get-TimeStamp {return "[{0:HH:mm:ss}]" -f (Get-Date)}
$StartTime = Get-Date -Format 'yyyy/MM/dd HH:mm:ss'
# Using a parameter and set it to accept pipeline input
Function Get-CInfo {
[CmdletBinding()]
Param (
[parameter(Mandatory = $true, ValueFromPipeline = $true)]$ComputerList
)
Begin {
$ErrorActionPreference = 'Stop'
Write-Host ""
Write-Host "Processing now: $StartTime"
}
Process {
foreach ($Computer in $ComputerList) {
Try {
Write-Host "$(Get-TimeStamp) Working on machine: $Computer"
# Gather data
$NetAdapter = gwmi -class "Win32_NetworkAdapterConfiguration" -cn $Computer | ? IpEnabled -EQ "True" | select MACAddress, IPaddress
$OStype = gwmi win32_operatingsystem -cn $computer | select Caption
$Serial = Get-WmiObject win32_bios -cn $computer | select Serialnumber
# Output custom object with required properties
[pscustomobject]#{
Computer = $Computer
#DNSHostName = $NetAdapter.DNSHostName;
MACAddress = $NetAdapter.MACAddress;
# Here is the line that I added [0] to the end
IPAddress = $NetAdapter.IPAddress[0];
OperatingSystem = $OSType.Caption;
Serial = $Serial.Serialnumber;
Error = ''
}
}
Catch {
# Within the catch section $_ always contains the error.
[pscustomobject]#{
Computer = $Computer
#DNSHostName = '';
MACAddress = '';
IPAddress = '';
OperatingSystem = '';
Serial = '';
Error = $_.Exception.Message
}
}
}#End of Loop
}
End {
Write-Host ""
Write-Host "*****"
Write-Host ""
Write-Host "Done"
Write-Host ""
}
}#End of the Function
# Pipe list to function and store to '$Results'
$Results = Get-Content .\comp-list.txt | Get-CInfo
# Output and formatting
# Format and output to text file
$Results | ft -AutoSize > comp-details.txt
# Or send to csv
$Results | Export-Csv -Path comp-details.csv -NoTypeInformation
# Output results to console
Get-Content -Path .\comp-details.csv
Here is the CSV output (redacted):
"Computer","MACAddress","IPAddress","OperatingSystem","Serial","Error"
"ComputerName001","xx:xx:xx:xx:xx:xx","123.456.789.000","Microsoft Windows 11 Enterprise","JJJJJJJ",""
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.
I'm in the early stages of learning powershell, and I'm trying to put together a script that remotely gathers information from our IIS servers, but I'm encountering several issues.
The first one is that the IP Address and OU columns remain empty in the output file.
The second one is that I'm not able to format the Administrator group column to have 1 group per line, or delimited by commas.
This is the current version of the code:
$computers = Get-Content "C:\servers.txt"
#Running the invoke-command on remote machine to run the iisreset
$output = foreach ($computer in $computers)
{
Write-Host "Details from server $computer..."
try{
Invoke-command -ComputerName $computer -ErrorAction Stop -ScriptBlock{
# Ensure to import the WebAdministration and AD module
Import-Module WebAdministration
Import-Module ActiveDirectory
$webapps = Get-WebApplication
$list = #()
foreach ($webapp in get-childitem IIS:\AppPools\)
{
$name = "IIS:\AppPools\" + $webapp.name
$item = #{}
$item.server = $env:computername
$item.WebAppName = $webapp.name
$item.Version = (Get-ItemProperty $name managedRuntimeVersion).Value
$item.State = (Get-WebAppPoolState -Name $webapp.name).Value
$item.UserIdentityType = $webapp.processModel.identityType
$item.Username = $webapp.processModel.userName
$item.Password = $webapp.processModel.password
$item.OU = (Get-ADComputer $_ | select DistinguishedNAme)
$item.IPAddress = (Get-NetIPAddress -AddressFamily IPv4)
$item.Administrators = (Get-LocalGroupMember -Group "Administrators")
$obj = New-Object PSObject -Property $item
$list += $obj
}
$list | select -Property "Server","WebAppName", "Version", "State", "UserIdentityType", "Username", "Password", "OU", "Ip Address", "Administrators"
}
} catch {
Add-Content .\failedservers.txt -Force -Value $computer
}
}
$output | Export-Csv -Path .\output.csv
#Stop-Transcript
I'd really appreciate any input on how to get it to work properly or improve on the code itself.
Thanks in advance!
For the OU property, you'll want to reference $env:COMPUTERNAME instead of $_:
$item.OU = (Get-ADComputer $env:COMPUTERNAME |Select -Expand DistinguishedName)
For the IPAddress and Administrators fields you'll want to use -join to create comma-separated lists of the relevant values:
$item.IPAddress = (Get-NetIPAddress -AddressFamily IPv4).IPAddress -join ','
$item.Administrators = (Get-LocalGroupMember -Group "Administrators").Name -join ','
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
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
}