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.
Related
I have following script to check the installed software on local and remote machines.
Usually it works fine but i have two problems. It only works when i open it in ISE. If i open it in a normal powershell, the script close immediately. Even a pause or read-host command won't work.
For example here is my script for a local machine. Hope you guys can help me.
Function Get-InstalledSoftware {
Param(
[Alias('Computer','ComputerName','HostName')]
[Parameter(
ValueFromPipeline=$True,
ValueFromPipelineByPropertyName=$true,
Position=1
)]
[string]$Name = $env:COMPUTERNAME
)
Begin{
$lmKeys = "Software\Microsoft\Windows\CurrentVersion\Uninstall","SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall"
$lmReg = [Microsoft.Win32.RegistryHive]::LocalMachine
$cuKeys = "Software\Microsoft\Windows\CurrentVersion\Uninstall"
$cuReg = [Microsoft.Win32.RegistryHive]::CurrentUser
}
Process{
if (!(Test-Connection -ComputerName $Name -count 1 -quiet)) {
Write-Error -Message "Unable to contact $Name. Please verify its network connectivity and try again." -Category ObjectNotFound -TargetObject $Computer
Break
}
$masterKeys = #()
$remoteCURegKey = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($cuReg,$computer)
$remoteLMRegKey = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($lmReg,$computer)
foreach ($key in $lmKeys) {
$regKey = $remoteLMRegKey.OpenSubkey($key)
foreach ($subName in $regKey.GetSubkeyNames()) {
foreach($sub in $regKey.OpenSubkey($subName)) {
$masterKeys += (New-Object PSObject -Property #{
"ComputerName" = $Name
"Name" = $sub.getvalue("displayname")
"SystemComponent" = $sub.getvalue("systemcomponent")
"ParentKeyName" = $sub.getvalue("parentkeyname")
"Version" = $sub.getvalue("DisplayVersion")
"UninstallCommand" = $sub.getvalue("UninstallString")
"InstallDate" = $sub.getvalue("InstallDate")
"RegPath" = $sub.ToString()
})
}
}
}
foreach ($key in $cuKeys) {
$regKey = $remoteCURegKey.OpenSubkey($key)
if ($regKey -ne $null) {
foreach ($subName in $regKey.getsubkeynames()) {
foreach ($sub in $regKey.opensubkey($subName)) {
$masterKeys += (New-Object PSObject -Property #{
"ComputerName" = $Name
"Name" = $sub.getvalue("displayname")
"SystemComponent" = $sub.getvalue("systemcomponent")
"ParentKeyName" = $sub.getvalue("parentkeyname")
"Version" = $sub.getvalue("DisplayVersion")
"UninstallCommand" = $sub.getvalue("UninstallString")
"InstallDate" = $sub.getvalue("InstallDate")
"RegPath" = $sub.ToString()
})
}
}
}
}
$woFilter = {$null -ne $_.name -AND $_.SystemComponent -ne "1" -AND $null -eq $_.ParentKeyName}
$props = 'Name','Version','ComputerName','Installdate','UninstallCommand','RegPath'
$masterKeys = ($masterKeys | Where-Object $woFilter | Select-Object $props | Sort-Object Name)
$masterKeys
}
End{}
}
Get-InstalledSoftware | select-object name | sort-object
Send your output somewhere, not just to the host window. I suspect the windows closes or your pause command is kicking in before its retrieved the results thus they aren't sent to the host window.
The following works fine for me run as a script:
Get-InstalledSoftware | Select-Object name | Sort-Object | Out-Gridview
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
The script runs correctly when outside of Start-Job but when in a scriptblock I get incorrect results. Where am I going wrong?
I need the Start-Job functionality since I have servers where the remote commands will hang (separate problem - WMI is borked) and I need to timeout and move to the next server.
I've tried every variation I can find in Google and still don't have the results I'm looking for.
I am really at my wits end with this as I don't understand what is happening... Help?
Thanks!
$timeoutSeconds = 90
ForEach($server in $servers) {
#$ErrorActionPreference = "inquire"
#$WarningPreference = "inquire"
$ErrorActionPreference = "silentlycontinue"
$WarningPreference = "silentlycontinue"
write-host $SERVER
$code = {
param($SERVER,$LOGto,$outputPath)
$ping = (Test-Connection -ComputerName $SERVER -Count 2 -Quiet )
if($ping -eq $true)
{
$pingVerbose = (Test-Connection -ComputerName $SERVER -Count 1)
$IP = $pingVerbose.IPV4Address.IPAddressToString
$osname2 = (Get-WMIObject -computerName $SERVER win32_operatingsystem).name
if($osname2 -match "|")
{
$osname,$osname1 = $osname2.Split('|')
} else {
$osname = $osname2
}
$lastinstalled = (Get-HotFix -computerName $SERVER | where -property InstalledOn -ne $null)
if($lastinstalled.InstalledOn)
{
$lastinstalledOn1 = ($lastinstalled.InstalledOn | Sort-Object -Property InstalledOn )[-1]
$lastinstalledOn = $lastinstalledOn1
}
$lastQFE = (get-wmiobject -class win32_quickfixengineering -computerName $SERVER | where -property InstalledOn -ne $null)
if($lastQFE.InstalledOn -ne $null)
{
$lastQFEon = ($lastQFE.InstalledOn | Sort-Object -Property InstalledOn)[-1]
$lastQFEon = $lastQFEon
}
if(($lastinstalledOn) -or ($lastQFEon))
{
if(($lastinstalledOn) -and ($lastinstalledOn -gt $lastQFEon))
{
$installedOn = $lastinstalledOn.tostring("MM/dd/yyyy")
$HotFixNumber = ($lastinstalled.HotFixID | Sort-Object -Property HotFixID)[-1]
} else {
$installedOn = $lastQFEon.tostring("MM/dd/yyyy")
$HotFixNumber = ($lastQFE.HotFixID | Sort-Object -Property HotFixID)[-1]
}
} else {
$installedOn = ''
$HotFixNumber = ''
}
}
#add entries to the log file
ac $outputPath\$LOGto "$Server,$ip,$installedOn,$HotFixNumber,$ping,$osname "
Write-Host "$Server,$ip,$installedOn,$HotFixNumber,$ping,$osname "
}
$runCode = Start-Job -ScriptBlock $code -ArgumentList $server,$LOGto,$outputPath
if(Wait-Job $runCode -Timeout $timeoutSeconds)
{
Receive-Job $runCode
} else {
Remove-Job -Force $runCode
ac $($outputPath + "\error.txt") "$Server"
}
}
When running in the scriptblock I receive the wrong date and KB.
SERVERNAME
SERVERNAME,10.1.XX.XX,03/13/2015,KB3022777,True,Microsoft Windows Server 2012 R2 Standard
vs.
SERVERNAME
SERVERNAME,10.1.XX.XX,05/15/2017,KB4012213,True,Microsoft Windows Server 2012 R2 Standard
For posterity more than anything else...
The error I was making was using sort-object against a string instead of a numerical value.
The code below will work correctly once you replace the domain name and file path information.
Thanks,
-Luke
#change this to a directory of your choice
$outputPath = "C:\Users\username\Desktop"
cd $outputPath
#create a default file name
$LOGto = "AllWinHotFixDateDC_$((Get-Date).ToString('yyyyMMdd')).csv"
#create the headers
sc .\$LOGto "Server,IPAddress,InstalledOn,HotFixNumber,Ping,OS_Name"
#get the server names from AD
Get-ADComputer -Filter {(Enabled -eq "True") -and (OperatingSystem -like "*Windows*") } -SearchBase "OU=Servers,DC=mydomain,DC=net" -server 'mydomain.net' -SearchScope Subtree | Select Name -ExpandProperty name | Sort-Object | Out-File .\servers.txt
$servers = get-content .\servers.txt
$timeoutSeconds = 90
ForEach($server in $servers) {
$ErrorActionPreference = "inquire"
$WarningPreference = "inquire"
#$ErrorActionPreference = "silentlycontinue"
#$WarningPreference = "silentlycontinue"
write-host $SERVER
$code = {
param($SERVER,$LOGto,$outputPath)
$ping = (Test-Connection -ComputerName $SERVER -Count 2 -Quiet )
if($ping -eq $true)
{
$pingVerbose = (Test-Connection -ComputerName $SERVER -Count 1)
$IP = $pingVerbose.IPV4Address.IPAddressToString
$osname2 = (Get-WMIObject -computerName $SERVER win32_operatingsystem).name
if($osname2 -match "|")
{
$osname,$osname1 = $osname2.Split('|')
} else {
$osname = $osname2
}
$getinstalled = (Get-HotFix -computerName $SERVER)
if($getinstalled)
{
if($getinstalled.HotFixID -ne $null)
{
$validinstalled = ($getinstalled.HotFixID -match "KB*")
$KB = (($validinstalled -replace 'KB','') | Sort-Object {[int]($_ -replace '(\d+).*', '$1')})[-1]
$lastinstalledOnHotFix = ("KB$KB")
}
if ($getinstalled.InstalledOn -ne $null)
{
$lastInstalled = ($getinstalled | Sort-Object -Property InstalledOn)[-1]
$lastInstalledlist = $lastInstalled.InstalledOn | Sort-Object {[int]($_ -replace '(\d+).*', '$1')}
$lastInstalledon = $lastInstalledlist.tostring("MM/dd/yyyy")
} else {
$lastinstalledOn = "0"
}
}
Write-Host $lastinstalledOn
Write-Host $lastinstalledOnHotFix
$getQFE = (get-wmiobject -class win32_quickfixengineering -computerName $SERVER )
if($getQFE)
{
if($getQFE.HotFixID -ne $null)
{
$validQFE = ($getQFE.HotFixID -match 'KB')
$KB = (($validQFE -replace 'KB','') | Sort-Object {[int]($_ -replace '(\d+).*', '$1')})[-1]
$lastQFEonHotFix = ("KB$KB")
}
if($getQFE.InstalledOn -ne $null)
{
$lastQFE = ($getQFE | Sort-Object -Property InstalledOn)[-1]
$lastQFElist = $lastQFE.InstalledOn | Sort-Object {[int]($_ -replace '(\d+).*', '$1')}
$lastQFEon = $lastQFElist.tostring("MM/dd/yyyy")
} else {
$lastQFEon = "0"
}
}
Write-Host $lastQFEon
Write-Host $lastQFEonHotFix
if(($lastinstalledOn -ne $null) -or ($lastQFEon -ne $null))
{
if(($lastInstalledlist -ne $null) -and ($lastInstalledlist -gt $lastQFElist))
{
$installedOn = $lastinstalledOn
$HotFixNumber = $lastinstalledOnHotFix
} elseif($lastQFEon -ne $null)
{
$installedOn = $lastQFEon
$HotFixNumber = $lastQFEonHotFix
}
} else {
$installedOn = '0'
$HotFixNumber = $lastQFEonHotFix
}
}
#add entries to the log file
ac $outputPath\$LOGto "$Server,$ip,$installedOn,$HotFixNumber,$ping,$osname "
Write-Host "$Server,$ip,$installedOn,$HotFixNumber,$ping,$osname "
}
$runCode = Start-Job -ScriptBlock $code -ArgumentList $server,$LOGto,$outputPath
if(Wait-Job $runCode -Timeout $timeoutSeconds)
{
Receive-Job $runCode
} else {
Remove-Job -Force $runCode
ac $($outputPath + "\error.txt") "$Server"
}
}
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 trying to create a script that can get the user profiles that haven't logged on a specific computer within 30 days NOT using active directory but my script didn't work. I am using Powershell version 3. This is my code:
netsh advfirewall firewall set rule group="Windows Management Instrumentation (WMI)" new enable=yes
$ComputerList = Get-Content C:\temp\Computers1.txt
$myDomain = Get-Content C:\temp\Domain.txt
$csvFile = 'C:\temp\Profiles.csv'
# Create new .csv output file
New-Item $csvFile -type file -force
# Output the field header-line to the CSV file
"HOST,PROFILE" | Add-Content $csvFile
# Loop over the list of computers from the input file
foreach ($Computer in $ComputerList) {
# see if ping test succeeds for this computer
if (Test-Connection $Computer -Count 3 -ErrorAction SilentlyContinue) {
$ComputerFQDN = $Computer + $myDomain
$Profiles = Get-WmiObject -Class Win32_UserProfile -Computer $ComputerFQDN | Where{$_.LocalPath -notlike "*$env:SystemRoot*"}
foreach ($profile in $profiles) {
try {
$objSID = New-Object System.Security.Principal.SecurityIdentifier($profile.LocalPath) | Where {((Get-Date)-$_.lastwritetime).days -ge 30}
#| Where-Object {$_.LastLogonDate -le $CurrentDate.AddDays(-60)}
$objuser = $objsid.Translate([System.Security.Principal.NTAccount])
$objusername = $objuser.value
} catch {
$objusername = $profile.LocalPath
}
switch($profile.status){
1 { $profileType="Temporary" }
2 { $profileType="Roaming" }
4 { $profileType="Mandatory" }
8 { $profileType="Corrupted" }
default { $profileType = "LOCAL" }
}
$User = $objUser.Value
#output profile detail for this host
"$($Computer.toUpper()), $($objusername)" | Add-Content $csvFile
}
} else {
#output failure message for this host
"$($Computer.toUpper()), PING TEST FAILED" | Add-Content $csvFile
}
#LOOP
}
I tried to change the -ge to -le in the line $objSID = New-Object System.Security.Principal.SecurityIdentifier($profile.LocalPath) | Where {((Get-Date)-$_.lastwritetime).days -ge 30}, as well as changing the range after it but it still gave me the same list of computers regardless of my changes.
There are a few problems with the script, most notable is that your use of Where-Object is testing an object (SID) that doesn't know anything about dates.
I would break it down a little differently. I would write a function to catch all the stuff I need to do to attempt to figure out the last logon. That's my goes in my stack of utility functions in case I need it again.
Then I have something to use that function which deals with implementing the logic for the immediate requirement.
So you end up with this. It's a bit long, see what you think.
function Get-LastLogon {
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline = $true)]
[String]$ComputerName = $env:COMPUTERNAME
)
process {
Get-WmiObject Win32_UserProfile -ComputerName $ComputerName -Filter "Special='FALSE'" | ForEach-Object {
# Attempt to get the UserAccount using WMI
$userAccount = Get-WmiObject Win32_UserAccount -Filter "SID='$($_.SID)'" -ComputerName $ComputerName
# To satisfy WMI all single \ in a path must be escaped.
# Prefer to use NTUser.dat for last modification
$path = (Join-Path $_.LocalPath 'ntuser.dat') -replace '\\', '\\'
$cimObject = Get-WmiObject CIM_DataFile -Filter "Name='$path'" -ComputerName $ComputerName
if ($null -eq $cimObject) {
# Fall back to the directory
$path = $_.LocalPath -replace '\\', '\\'
$cimObject = Get-WmiObject CIM_Directory -Filter "Name='$path'" -ComputerName $ComputerName
}
$lastModified = $null
if ($null -ne $cimObject) {
$lastModified = [System.Management.ManagementDateTimeConverter]::ToDateTime($cimObject.LastModified)
}
# See if LastUseTime is more useful.
$lastUsed = $null
if ($null -ne $_.LastUseTime) {
$lastUsed = [System.Management.ManagementDateTimeConverter]::ToDateTime($_.LastUseTime)
}
# Profile type
$profileType = switch ($_.Status) {
1 { "Temporary" }
2 { "Roaming" }
4 { "Mandatory" }
8 { "Corrupted" }
0 { "LOCAL" }
}
[PSCustomObject]#{
ComputerName = $ComputerName
Username = $userAccount.Caption
LastChanged = $lastModified
LastUsed = $lastUsed
SID = $_.SID
Path = $_.LocalPath
ProfileType = $profileType
}
}
}
}
$myDomain = Get-Content C:\temp\Domain.txt
Get-Content C:\temp\Computers1.txt | ForEach-Object {
$ComputerName = $_ + $myDomain
if (Test-Connection $ComputerName -Quiet -Count 3) {
Get-LastLogon -ComputerName $ComputerName | Select-Object *, #{Name='Status';Expression={ 'OK' }} |
Where-Object { $_.LastChanged -lt (Get-Date).AddDays(-30) }
} else {
# Normalise the output so we don't lose columns in the export
$ComputerName | Select-Object #{Name='ComputerName';e={ $ComputerName }},
Username, LastChanged, LastUsed, SID, Path, ProfileType, #{Name='Status';Expression={ 'PING FAILED' }}
}
} | Export-Csv 'C:\temp\Profiles.csv' -NoTypeInformation