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
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",""
My workflow:
check if server is pingable
find if they are domain connected or not and perform a task accordingly. if Operating system 2012 and/or R2 ,2016 or 2019 newer OSes then I will run Get-SmbServerConfiguration cmdlet. if machine is not a part of default domain then else block will run.
if Operating system 2003 or 2008 oldest OSes then I will run Get-Wmi cmdlet. if machine is not a part of default domain then else block will run.
Finally , I will concentanate $results variable.
My question is :
1- How can we get remotely regedit value for 2003 or 2008 oldest OSes IS NOT a part of default domain insie else block?
Also , Condition will be like below.
if SMB1 value is "0" then result will be `false`
if SMB1 value is "1" then result will be `true`
if SMB1 value is not exist then result will be `not exist value`
2- How can I create object properties $SMBAudit variable ? because , I will concentanate all outputs inside $results variable.
$reg = [wmiclass]"\\$computer\root\default:StdRegProv"
$SMBAudit = $reg.GetStringValue($basekey, $subkey, $value).sValue
My desired output :
Computername,SMB1Enabled
Host01,True
Host02,False
I will write so far a script like below. but I am stucking somethings.
Script :
# Computer List
$allComputers = Get-Content .\path\to\computers.txt
read-host -assecurestring | convertfrom-securestring | out-file C:\mysecurestring_domain.txt
read-host -assecurestring | convertfrom-securestring | out-file C:\mysecurestring_local.txt
# Create empty array of results
$Results = #()
# Loop through computers
foreach($computer in $allComputers) {
# check if server is pingable before running the query on the server
if (Test-Connection $computer -Count 1 -Quiet) {
Write-Host "`n`n$computer is online!" -BackgroundColor Green -ForegroundColor Black
}
if(Get-ADComputer -Filter {Name -eq $computer -and OperatingSystem -notlike '*Windows*Server*2003*' -and OperatingSystem -notlike '*Windows*Server*2008*'})
{
#"machine $_ is a part of default domain"
# The command we want to run
$username = "domain01\admin01"
$password = Get-Content 'C:\mysecurestring_domain.txt' | ConvertTo-SecureString
$cred = new-object -typename System.Management.Automation.PSCredential -argumentlist $username, $password
$SMB = Invoke-Command -ComputerName $computer -Credential $cred -ScriptBlock {Get-SmbServerConfiguration | Select EnableSMB1Protocol }
# Create properties
$Properties = #{
# Populate the properties "Computername" and "SMB1Enabled" with variables
Computername = $Computer
SMB1Enabled = $SMB.EnableSMB1Protocol
}
# Add the properties to the result for each object
$Results += New-Object psobject -Property $Properties
}
else
{
#"machine $_ IS NOT a part of default domain"
$username = "localadmin01"
$password = Get-Content 'C:\mysecurestring_local.txt' | ConvertTo-SecureString
$cred = new-object -typename System.Management.Automation.PSCredential -argumentlist $username, $password
$SMB = Invoke-Command -ComputerName $computer -Credential $cred -ScriptBlock {Get-SmbServerConfiguration | Select EnableSMB1Protocol }
# Create properties
$Properties = #{
# Populate the properties "Computername" and "SMB1Enabled" with variables
Computername = $Computer
SMB1Enabled = $SMB.EnableSMB1Protocol
}
# Add the properties to the result for each object
$Results += New-Object psobject -Property $Properties
}
# Oldest OSes
if(Get-ADComputer -Filter {Name -eq $computer -and OperatingSystem -notlike '*Windows*Server*2012*' -and OperatingSystem -notlike '*Windows*Server*2016*' -and OperatingSystem -notlike '*Windows*Server*2019*'})
{
#"machine $_ is a part of default domain"
# The command we want to run
<# HKEY_CLASSES_ROOT (2147483648 (0x80000000))
HKEY_CURRENT_USER (2147483649 (0x80000001))
HKEY_LOCAL_MACHINE (2147483650 (0x80000002))
HKEY_USERS (2147483651 (0x80000003))
HKEY_CURRENT_CONFIG (2147483653 (0x80000005))
#>
$basekey = [uint32]'0x80000002'
$subkey = 'SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters'
$value = 'SMB1'
$reg = [wmiclass]"\\$computer\root\default:StdRegProv"
$SMBAudit = $reg.GetStringValue($basekey, $subkey, $value).sValue
}
else
{
#"machine $_ IS NOT a part of default domain"
}
# Output
$Results | Select-Object Computername, SMB1Enabled | Out-File -Filepath c:\temp\smb1-computers.txt
I think you are over complicating this and although not tested by me, you could try this:
# Computer List
$allComputers = Get-Content '.\path\to\computers.txt'
# get credentials for domain-joined machines and for local machines
$domainCred = Get-Credential -UserName "domain01\admin01" -Message "Please enter the DOMAIN password"
$localCred = Get-Credential -UserName "localadmin01" -Message "Please enter the LOCAL password"
# loop through the list of computers and collect output in variable $Results
$Results = foreach($computer in $allComputers) {
# check if server is pingable before running the query on the server
if (Test-Connection -ComputerName $computer -Count 1 -Quiet) {
Write-Host "$computer is online!" -BackgroundColor Green -ForegroundColor Black
$server = Get-ADComputer -Filter "Name -eq '$computer'" -Properties OperatingSystem -ErrorAction SilentlyContinue
# if domain joined, use $domainCred, otherwise $localCred
if ($server) {
$cred = $domainCred
$version = ([regex]'Windows Server (\d+)').Match($server.OperatingSystem).Groups[1].Value
}
else {
$cred = $localCred
$info = Get-WmiObject -ComputerName $computer -Credential $cred -Class Win32_OperatingSystem
$version = ([regex]'Windows Server (\d+)').Match($info.Caption).Groups[1].Value
}
if ($version -eq '2003') {
# try reading the registry
try {
$RegBase = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $Computer)
$RegKey = $RegBase.OpenSubKey("SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters")
$SMB = $RegKey.GetValue("SMB1")
[PsCustomObject]#{ ComputerName = $computer; SMB1Enabled = ($null -eq $SMB -or [int]$SMB -eq 1) }
}
catch {
[PsCustomObject]#{ ComputerName = $computer; SMB1Enabled = 'Could not read Remote Registry' }
}
finally {
if ($RegBase) { $RegBase.Close() }
if ($RegKey) { $RegKey.Close() }
}
}
elseif ($version -eq '2008') {
# Older OS
try {
# try via WinRM
$SMB = Invoke-Command -ComputerName $computer -Credential $cred -ScriptBlock {
Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters' -Name SMB1
} -ErrorAction Stop
# output an object
[PsCustomObject]#{ ComputerName = $computer; SMB1Enabled = ($null -eq $SMB -or [int]$SMB -eq 1) }
}
catch {
# try reading the registry
try {
$RegBase = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $Computer)
$RegKey = $RegBase.OpenSubKey("SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters")
$SMB = $RegKey.GetValue("SMB1")
[PsCustomObject]#{ ComputerName = $computer; SMB1Enabled = ($null -eq $SMB -or [int]$SMB -eq 1) }
}
catch {
[PsCustomObject]#{ ComputerName = $computer; SMB1Enabled = 'Could not read Remote Registry' }
}
finally {
if ($RegBase) { $RegBase.Close() }
if ($RegKey) { $RegKey.Close() }
}
}
}
else {
# Newer OS
$SMB = Invoke-Command -ComputerName $computer -Credential $cred -ScriptBlock { Get-SmbServerConfiguration | Select-Object EnableSMB1Protocol }
# output an object
[PsCustomObject]#{ ComputerName = $computer; SMB1Enabled = $SMB.EnableSMB1Protocol }
}
}
else {
Write-Warning "Computer $computer is off-line"
# output an object anyway, so that in the CSV it is known that the computer didn't ping
[PsCustomObject]#{ ComputerName = $computer; SMB1Enabled = 'Off-Line' }
}
}
# Output on screen
$Results | Format-Table -AutoSize
# Output to CSV file
$Results | Export-Csv -Path 'c:\temp\smb1-computers.csv' -NoTypeInformation -UseCulture
What I am trying to achieve here is add the servers and the updates that are not installed on the server to an array and create a new object that is going to display the names of the servers in one column and the missing updates on another column, but at the end I am getting an empty Grid-View table.
The values for the servers and updates are read from a file.
Write-Host
#Read the password from stdin and store it in a variable
$password = Read-Host -AsSecureString -Prompt "Enter your password"
Write-Host
#Get credentials and password for later user
$cred = New-Object System.Management.Automation.PSCredential ("Administrator#testing.local", $password )
#Get the list of available servers to test
$servers = Get-Content -Path $HOME\Desktop\servers.txt
#Get the list of available updates that need to be installed on the server
$available_updates = Get-Content $HOME\Desktop\update.txt
$add_updates = #()
$add_updates_and_servers = #()
#Get each server name from the list and execute the following commands
foreach ($server in $servers) {
#Test if the server is reponding
$ping = Test-Connection $server -Count 1 -Quiet
#If the above command returns True continue
if ($ping -eq "True") {
#Write a message saying Testing server_name
Write-Host "Testing $server"
foreach ($update in $available_updates) {
#Check if update is installed
$updates_from_os = Invoke-Command -ComputerName $server -Credential $cred -ScriptBlock { Get-HotFix | Select-Object -Property HotFixID | Where-Object -Property HotFixID -EQ $Using:update } -HideComputerName | Select-Object -ExpandProperty HotFixID
if (!$updates_from_os) {
$add_updates += $update
}
}
New-Object -TypeName PSObject -Property $updates -OutVariable final
$updates = #{
"Server" = $server
"Updates" = $add_updates
}
}
$add_updates_and_servers += $final
}
$add_updates_and_servers | Out-GridView
For what is probably happening with your script:
I suspect that each time you calling the statement New-Object -TypeName PSObject -Property $updates -OutVariable final You overwriting any previous created $final object which references to the same objects as your $add_updates_and_servers collection.
Anyways, try to avoid using the increase assignment operator (+=) to create a collection, instead stream the results to a variable (or even better, directly to next/final cmdlet: ... }| Out-GridView).
Something like:
$add_updates_and_servers = foreach ($server in $servers) {
$ping = Test-Connection $server -Count 1 -Quiet
if ($ping -eq "True") {
Write-Host "Testing $server"
$add_updates = #(
foreach ($update in $available_updates) {
$updates_from_os = Invoke-Command -ComputerName $server -Credential $cred -ScriptBlock { Get-HotFix | Select-Object -Property HotFixID | Where-Object -Property HotFixID -EQ $Using:update } -HideComputerName | Select-Object -ExpandProperty HotFixID
if (!$updates_from_os) { $update }
}
)
[PSCustomObject]#{
"Server" = $server
"Updates" = $add_updates
}
}
}
Note: in case you want each $update in a separate column, also have a look at: Not all properties displayed
I am trying to get the .NetFramwork version from all the windows servers. I am using powershell script. I can get the output displayed but unable to get the output from the hashtable to a output file. Also how would I get rid of the "..." from VersionDetails : {1.0.3705, 1.1.4322, 2.0.50727, 3.0...} and show the full content.
Any help will be greatly appreciated
here is the code I am using:
$username = "username"
$password = "Password"
$secstr = New-Object -TypeName System.Security.SecureString
$password.ToCharArray() | ForEach-Object {$secstr.AppendChar($_)}
$cred = new-object -typename System.Management.Automation.PSCredential -argumentlist $username, $secstr
$query = "select name from win32_directory where name like 'c:\\windows\\microsoft.net\\framework\\v%'"
$ComputerNames = Get-Content "d:\Scripts\serverList.txt"
foreach ($ComputerName in $ComputerNames)
{
write-host "ComputerName = $ComputerName"
$ComputerName | ForEach-Object {
$res = Get-WmiObject -query $query -Credential $cred -ComputerName $ComputerName | ForEach-Object {
Split-Path $_.name -Leaf } | # returns directories
Where-Object { $_ -like 'v*' } | # only include those that start with v
ForEach-Object { [system.version]( $_ -replace "^v" ) }
# remove "v" from the string and convert to version object
# Create hashtable with computername and version details
$prop = #{
ComputerName = $ComputerName
#V1_Present = &{ if ( $res | Where-Object { $_.Major -eq 1 -and $_.Minor -eq 0 } ) { $true } }
#V1_1Present = &{ if ( $res | Where-Object { $_.Major -eq 1 -and $_.Minor -eq 1 } ) { $true } }
V2_Present = &{ if ( $res | Where-Object { $_.Major -eq 2 -and $_.Minor -eq 0 } ) { $true } }
V3_Present = &{ if ( $res | Where-Object { $_.Major -eq 3 -and $_.Minor -eq 0 } ) { $true } }
V3_5Present = &{ if ( $res | Where-Object { $_.Major -eq 3 -and $_.Minor -eq 5 } ) { $true } }
V4_Present = &{ if ( $res | Where-Object { $_.Major -eq 4 -and $_.Minor -eq 0 } ) { $true } }
VersionDetails = $res
}
# Create and output PSobject using hashtable
New-Object PSObject -Property $prop
}
=========================================================
Output dispalys
PS D:\Scripts> .\GetDotNetFrameworkver.ps1
in for loop ComputerName = XXXXXXX
V4_Present : True
V3_5Present : True
V2_Present : True
V3_Present : True
ComputerName : XXXXX
VersionDetails : {1.0.3705, 1.1.4322, 2.0.50727, 3.0...}
Based on the answer of link there is a "simpler" (and faster) solution to fetch the versions.
Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP' -recurse | Get-ItemProperty -name Version,Release -ErrorAction Ignore | Where { $_.PSChildName -match '^(?!S)\p{L}'} | Select PSChildName, Version, Release
If you want to get the versions of different remote machines you can use PowerShell remoting. Be aware that you've to enable PS remoting .If your OS version is WIN10/WIN2012R2 it is enabled per default. If you're using an older OS you've to call Enable-PSRemoting on the remote machine. See this link for details.
Example:
$result = Invoke-Command -ComputerName computer1.domain, computer1.domain -Credential (Get-Credential ) -ScriptBlock {
Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP' -recurse | Get-ItemProperty -name Version,Release -ErrorAction Ignore | Where { $_.PSChildName -match '^(?!S)\p{L}'} | Select PSChildName, Version, Release
}
$hash = $result | group PSComputerName -AsHashTable # Group the .Net versions by computername
$hash.'computer1.domain' # Print all .Net version of computer1
$hash.'computer1.domain'.Version # Only print the version
Hope that helps.
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
}