PowerShell: Update Windows Computers

<# Update-Windows-Computers-v0.11.ps1
#
# Purpose: trigger Microsoft Updates on a list of Windows machines. Reboot each computer when necessary.
#
# Assumptions:
# - All computers in the list are joined to the same domain
# - Remote Powershell (WinRM) is accessible at the target systems
#
# Workflow:
# - Run in the context of a System Elevated session (Administrator)
# - Check whether the computer is domain joined
# - Validate the current user as a domain admin. If not, obtain domain admin credentials from the user.
# - Retrieve a list of computer names from a text file located in the same directory as this script
# - Prompt the user on whether to perform Windows update on the local system, remote system, or all [listed & local]
#>

############# Ensure that Program executes in the context of a Local System Administrator ################
# Get the ID and security principal of the current user account
$myWindowsID=[System.Security.Principal.WindowsIdentity]::GetCurrent()
$currentUser=$myWindowsID.Name
$myWindowsPrincipal=new-object System.Security.Principal.WindowsPrincipal($myWindowsID)

# Get the security principal for the Administrator role
$adminRole=[System.Security.Principal.WindowsBuiltInRole]::Administrator

# Check to see if we are currently running "as Administrator"
if ($myWindowsPrincipal.IsInRole($adminRole))
{
# We are running "as Administrator" - so change the title and background color to indicate this
$Host.UI.RawUI.WindowTitle = $myInvocation.MyCommand.Definition + "(Elevated)"
$Host.UI.RawUI.BackgroundColor = "Black"
clear-host
}
else
{
# We are not running "as Administrator" - so relaunch as administrator

# Create a new process object that starts PowerShell
$newProcess = new-object System.Diagnostics.ProcessStartInfo "PowerShell";

# Specify the current script path and name as a parameter
$newProcess.Arguments = $myInvocation.MyCommand.Definition;

# Indicate that the process should be elevated
$newProcess.Verb = "runas";

# Start the new process
[System.Diagnostics.Process]::Start($newProcess);

# Exit from the current, unelevated, process
exit
}

function validateCurrentAccountAsDomainAdmin{
if((whoami /groups) -match 'domain admins'){
#"This account is a Domain Admins member";
return $True;
}else{
#"This account is NOT a Domain Admins member";
return $False;
}
}

Write-Host -NoNewLine "Running as $currentUser in context of Elevated System Permissions..."
Write-Host "$currentUser $(if(validateCurrentAccountAsDomainAdmin){"is"}else{"not"}) a Domain Admin."
##########################################################################################################

############# Ensure that Program executes in the context of a Domain Administrator ######################

# This function is will export a global variable $domainAdminCred of a Domain Administrator account
Function getDomainAdminCred{

# Add pre-requisites and obtain list of domain admins
if (!(Get-Module ActiveDirectory)){
Import-Module ServerManager;
Add-WindowsFeature RSAT-AD-PowerShell;
Import-Module ActiveDirectory;
}

# Set domain variables
$currentUser=[System.Security.Principal.WindowsIdentity]::GetCurrent().Name
$userDomain=$env:USERDOMAIN
$domainObject = "LDAP://" + $env:userdnsdomain
$domainAdmins=Get-ADGroupMember -Identity "Domain Admins" -Recursive | %{Get-ADUser -Identity $_.distinguishedName} | Where-Object {$_.Enabled -eq $True}|%{"$userDomain\$($_.SamAccountName)"}

# Check whether a given username matches the list of Domain Admins
function validateDomainAdminMembership{
param (
[string]$username=$currentUser
)
if($domainadmins|?{$_ -eq $username}){
Write-Host "$username is a Domain Admin";
return $True;
}else{
Write-Host "$username not a Domain Admin.";
return $False;
}
}

function testCredential{
param (
[string]$username=$currentUser,
$encryptedPassword=$currentUserPassword
)
$plaintextPassword = (New-Object System.Management.Automation.PSCredential 'N/A',$encryptedPassword).GetNetworkCredential().Password
$domainBindTest = (New-Object System.DirectoryServices.DirectoryEntry($domainObject,$username,$plaintextPassword)).DistinguishedName
if ($domainBindTest){return $True;} else{Return $False;}
}

function validateDomainAdminCred{
$global:domainAdminCred=$False;

function loopUntilCred{
# Get the domain admin account name
do {
$newID=Read-Host -Prompt 'Input a domain admin username'
if (!($newID -match $userDomain)){$newID="$userDomain\$newID"} # Add domain prefix to userID if input doesn't already contain such
$domainAdminMatched=validateDomainAdminMembership $newID;
} until ($domainAdminMatched)

# Get the correct password
do {
$newPassword = Read-Host -assecurestring "Please enter the password for $newID"
#$providedPassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($password))
#$providedCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $providedID,$providedPassword
$goodCredential=testCredential -username $newID -encryptedPassword $newPassword
if($goodCredential){
"Alternate Domain Admin Credential validated!";
$global:domainAdminCred=New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $newID,$newPassword;
}
else{
"Password doesn't match.";
}
} until($goodCredential)
}

if (validateDomainAdminMembership){
$currentUserPassword=Read-Host -assecurestring "Please enter the current user's password";
if(testCredential -encryptedPassword $currentUserPassword){
"Domain Admin cred with current account has been validated!";
$GLOBAL:domainAdminCred=New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $currentUser,$currentUserPassword;
}else{loopUntilCred;}
}else{
loopUntilCred;
}
}
validateDomainAdminCred;
return $domainAdminCred;
}

function computerIsDomainJoined{
if ((gwmi win32_computersystem).partofdomain -eq $true) {
write-host -fore green "$ENV:computername is domain joined!"
return $true;
} else {
write-host -fore red "$ENV:computername is on a workgroup!"
return $false;
}
}

if (computerIsDomainJoined){
getDomainAdminCred;
}else{"This computer is not joined to a domain. Thus no Domain Admin credentials are expected."}
##########################################################################################################

# Get script path
$scriptName=$MyInvocation.MyCommand.Path
$scriptPath=Split-Path -Path $scriptName

# Update this list here via direct text editing or change it to pull a prepared text/csv file
#$computersList=Get-Content -Path "$scriptPath\computerslist.txt"
$computersList="SHERVER01","SHERVER02","SHERVER03","SHERVER04"

# Remove this computer from the remote computers list, if exists
$thisComputer=$ENV:computername
$remoteComputers=$computersList|?{$_ -ne $thisComputer}

function updateLocalWindows{
# Prerequisites
function installPrerequisites{
#Import-Module PSWindowsUpdate -force;
#$psWindowsUpdateAvailable=Get-Module PSWindowsUpdate -EA SilentlyContinue;
$psWindowsUpdateAvailable=Get-Module -ListAvailable -Name PSWindowsUpdate -ErrorAction SilentlyContinue;
if (!($psWindowsUpdateAvailable)){
try {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12;
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Confirm:$false | Out-Null;
Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted | Out-Null;
Install-Module PSWindowsUpdate -Confirm:$false -Force | Out-Null;
Import-Module PSWindowsUpdate -force | Out-Null;
}
catch{
"Prerequisites not met on $computer.";
}
}
}

function checkPendingReboot{
param([string]$computer=$ENV:computername)

function checkRegistry{
if (Get-ChildItem "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending" -EA Ignore) { return $true }
if (Get-Item "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired" -EA Ignore) { return $true }
if (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" -Name PendingFileRenameOperations -EA Ignore) { return $true }
try {
$util = [wmiclass]"\\.\root\ccm\clientsdk:CCM_ClientUtilities"
$status = $util.DetermineIfRebootPending()
if(($status -ne $null) -and $status.RebootPending){
return $true
}
}catch{}
return $false
}

$localhost=$ENV:computername
if ($localHost -eq $computer){
$result=checkRegistry;
}else{
$result=Invoke-Command -ComputerName $computer -ScriptBlock{
param($importedFunc);
[ScriptBlock]::Create($importedFunc).Invoke();
} -ArgumentList ${function:checkRegistry}
}
return $result;
}
installPrerequisites;

# Register the user of Windows Update Service if it has not been registered
$MicrosoftUpdateID="7971f918-a847-4430-9279-4a52d1efe18d"
$registered=$MicrosoftUpdateID -in (Get-WUServiceManager).ServiceID
if (!($registered)){
Add-WUServiceManager -ServiceID 7971f918-a847-4430-9279-4a52d1efe18d -Confirm:$false
}

# Perform Updates
Get-WindowsUpdate -AcceptAll -MicrosoftUpdate -Install -IgnoreReboot;

if (checkPendingReboot){
$warning="There is a pending reboot flag on this host.`n"
$prompt="Please type 'exit' to cancel or 'reboot' to reboot"
$warning;
do{
$userInput=Read-Host -Prompt $prompt;
if ($userInput -match "reboot"){"Restarting command received!";Restart-Computer;} # -match is faster than -like
}while (($userInput -notmatch "reboot") -AND ($userInput -notmatch "(quit|cancel|exit)"))
}else{"Done."}
}

function listPendingUpdates{
#[DateTime]$last24Hours=(Get-Date).AddHours(-24)
param([string]$computername = $env:COMPUTERNAME)

"Checking computer $computername`:"
$updatesession = [activator]::CreateInstance([type]::GetTypeFromProgID("Microsoft.Update.Session",$computername))
$UpdateSearcher = $updatesession.CreateUpdateSearcher()
$searchresult = $updatesearcher.Search("IsInstalled=0") # 0 = NotInstalled | 1 = Installed

$updates = If ($searchresult.Updates.Count -gt 0) {
$count = $searchresult.Updates.Count
For ($i=0; $i -lt $count; $i++) {
#$timeStamp=[DateTime]$item.LastDeploymentChangeTime
$item = $searchresult.Updates.Item($i)
#if ($timeStamp -gt $last24Hours){
#Create new custom object
[pscustomobject]@{
Title = $item.Title
KB = $item| foreach {$_.KbArticleids}
Severity = $item.MsrcSeverity
IsDownloaded=$item.IsDownloaded
SecurityBulletin = $($item.SecurityBulletinIDs)
Url = $item.MoreInfoUrls
Categories = ($item.Categories | Select-Object -ExpandProperty Name)
BundledUpdates = @($item.BundledUpdates)|ForEach{
[pscustomobject]@{
Title = $_.Title
DownloadUrl = @($_.DownloadContents).DownloadUrl
}

}
}#pscustomobject
#}#if-timeStamp
}#forloop
}#updates

if($updates){$updates |ft kb,title,severity,IsDownloaded -autosize}else{"No pending updates."}
}

function updateRemoteWindows{
[CmdletBinding()]
param (
[parameter(Mandatory=$true,Position=0)]
[string[]]$computer=$ENV:computername
)

<#
.SYNOPSIS
This script will automatically install all avaialable windows updates on a device and will automatically reboot if needed, after reboot, windows updates will continue to run until no more updates are available.
.PARAMETER URL
User the Computer parameter to specify the Computer to remotely install windows updates on.
# Around 40% of this function was forked from https://www.altaro.com/msp-dojo/powershell-windows-updates/
# Partial credit: Luke Orellana
# Note: I've added more features:
# - Check WSUS settings to account for that scenario
# - Prepare the targets by installing prerequisites prior to proceeding further to preemptively resolve dependency errors
# - Include additional dedendencies such as TLS1.2, Nuget & PSGALLERY
# - Check if server needs a reboot before issuing the reboot command instead of just inadvertently trigger reboots
# - Fixed the blank lines in output log causing bug in status query
# - More thorough cleanup routine
# - Detect and handle proxies (in development)
#>

function installPrerequisites{
#Import-Module PSWindowsUpdate -force;
#$psWindowsUpdateAvailable=Get-Module PSWindowsUpdate -EA SilentlyContinue;
$psWindowsUpdateAvailable=Get-Module -ListAvailable -Name PSWindowsUpdate -ErrorAction SilentlyContinue;
if (!($psWindowsUpdateAvailable)){
try {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12;
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Confirm:$false | Out-Null;
Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted | Out-Null;
Install-Module PSWindowsUpdate -Confirm:$false -Force | Out-Null;
Import-Module PSWindowsUpdate -force | Out-Null;
}
catch{
"Prerequisites not met on $ENV:COMPUTER.";
}
}
}

function checkPendingReboot{
param([string]$computer=$ENV:computername)

function checkRegistry{
if (Get-ChildItem "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending" -EA Ignore) { return $true }
if (Get-Item "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired" -EA Ignore) { return $true }
if (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" -Name PendingFileRenameOperations -EA Ignore) { return $true }
try {
$util = [wmiclass]"\\.\root\ccm\clientsdk:CCM_ClientUtilities"
$status = $util.DetermineIfRebootPending()
if(($status -ne $null) -and $status.RebootPending){
return $true
}
}catch{}
return $false
}

$localhost=$ENV:computername
if ($localHost -eq $computer){
$result=checkRegistry;
}else{
$result=Invoke-Command -ComputerName $computer -Credential $domainAdminCred -ScriptBlock{
param($importedFunc);
[ScriptBlock]::Create($importedFunc).Invoke();
} -ArgumentList ${function:checkRegistry}
}
return $result;
}

# Function requires 2 parameters: -computerName and -processName
function killProcess{
[CmdletBinding()]
param(
[string[]]$computerName=$ENV:COMPUTERNAME,
[parameter(Mandatory=$true)][string]$executableName="powershell.exe"
)

# WMI querying method
$processes = Get-WmiObject -Class Win32_Process -ComputerName $ComputerName -Credential $domainAdminCred -Filter "name='$executableName'"

if ($processes){
foreach ($process in $processes) {
$terminationResult = $process.terminate()
$processid = $process.handle

if($terminationResult.returnvalue -eq 0) {
write-host "The process $executableName `($processid`) terminated successfully"
} else {
write-host "The process $executableName `($processid`) termination has some problems"
}
}
}else{
"$executableName is currently not running on $computerName."
}
}

# installPrerequisites on remote machine
<#
invoke-command -computername $computer -Credential $domainAdminCred -scriptblock{
param($importedFunc);
[ScriptBlock]::Create($importedFunc).Invoke();
} -Args ${function:installPrerequisites};
#>

Do{
#starts up a remote powershell session to the computer
do{
$session = New-PSSession -ComputerName $computer -Credential $domainAdminCred
"Connecting to remote computer $computer..."
sleep -seconds 5
if ($session){"Connected."}
} until ($session.state -match "Opened")

# Install prerequisites
invoke-command -session $session -scriptblock { param($importedFunc);
[ScriptBlock]::Create($importedFunc).Invoke();
} -Args ${function:installPrerequisites};

#retrieves a list of available updates
"Checking for new updates on $computer"
$updates = invoke-command -session $session -scriptblock {Get-wulist -verbose}

# Count how many updates are available
$updatesCount = ($updates.kb).count

function checkWsus{
# Check if this machine has WSUS settings configured
$wuPath="Registry::HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU";
$wuKey="UseWUServer";
$wuIsOn=(Get-ItemProperty -path $wuPath -name $wuKey -ErrorAction SilentlyContinue).$wuKey;
if($wuIsOn){$GLOBAL:wsus=$True}else{$GLOBAL:wsus=$False}
}

function turnoffWsus{
# Turn WSUS settings OFF temporarily...
$wuPath="Registry::HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU";
$wuKey="UseWUServer";
setRegKey -path $wuPath -name $wuKey -value 0;
restart-service wuauserv;
}

function turnonWsus{
# Turning WSUS settings back to ON status
$wuPath="Registry::HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU";
$wuKey="UseWUServer";
setRegKey -path $wuPath -name $wuKey -value 1;
restart-service wuauserv;
}

# If there are available updates proceed with installing the updates and then reboot the remote machine if required
if ($updates -ne $null){

checkWsus;
if($wsus){turnoffWsus;}
# Create a scheduled task on remote computer name PSWindowsUpdate with this script
<# experimental: currently, this prevented scheduled task from executing
$invokeScript = {
# Check if this machine has WSUS settings configured
$wuPath="Registry::HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU";
$wuKey="UseWUServer";
$wuIsOn=(Get-ItemProperty -path $wuPath -name $wuKey -ErrorAction SilentlyContinue).$wuKey;

if($wuIsOn){
# Perform updates by manipulating WSUS keys
# Turn WSUS settings OFF temporarily...
setRegKey -path $wuPath -name $wuKey -value 0;
restart-service wuauserv;
Import-Module PSWindowsUpdate;
Get-WindowsUpdate -AcceptAll -Install | Out-File C:\PSWindowsUpdate.log;
# Turning WSUS settings back to ON status
setRegKey -path $wuPath -name $wuKey -value 1;
restart-service wuauserv;
}else{
"Triggering Updates...";
Import-Module PSWindowsUpdate;
Get-WindowsUpdate -AcceptAll -Install | Out-File C:\PSWindowsUpdate.log;
(gc C:\PSWindowsUpdate.log) | ? {$_.trim() -ne "" } | set-content C:\PSWindowsUpdate.log;
}
}
#>
#Invoke-WUJob will insert a scheduled task on the remote target as a mean to bypass 2nd hop issues
$invokeScript = {import-module PSWindowsUpdate; Get-WindowsUpdate -AcceptAll -Install | Out-File C:\PSWindowsUpdate.log}
$job=Invoke-WUjob -ComputerName $computer -Credential $domainAdminCred -Script $invokeScript -Confirm:$false -RunNow
#[void]$job;
<# Troubleshooting
ERROR:
Invoke-WUjob : [SERVER] Connecting to remote server SERVER failed with the following error message :
The WinRM client cannot process the request. Default credentials with Negotiate over HTTP can be used only if the
target machine is part of the TrustedHosts list or the Allow implicit credentials for Negotiate option is specified.
For more information, see the about_Remote_Troubleshooting Help topic.
At line:49 char:9
+ Invoke-WUjob -ComputerName $computer -Script $Script -Confirm ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OpenError: (PRDFCL01DC1SV1:String) [Invoke-WUJob], PSRemotingTransportException
+ FullyQualifiedErrorId : ExplicitCredentialsRequired,PSSessionStateBroken


Resolution:
1. Option one is to ignore this error. No actions are required as this error appears to have no effects
on Invoke-WUjob's intended scheduled task creation.
Hence, the remote updating program proceeds as normal.
2. Option two is to fix the targets using this information

-----------------------------------------------------
HOW TO USE AN IP ADDRESS IN A REMOTE COMMAND
-----------------------------------------------------
ERROR: The WinRM client cannot process the request. If the
authentication scheme is different from Kerberos, or if the client
computer is not joined to a domain, then HTTPS transport must be used
or the destination machine must be added to the TrustedHosts
configuration setting.

The ComputerName parameters of the New-PSSession, Enter-PSSession and
Invoke-Command cmdlets accept an IP address as a valid value. However,
because Kerberos authentication does not support IP addresses, NTLM
authentication is used by default whenever you specify an IP address.

When using NTLM authentication, the following procedure is required
for remoting.

1. Configure the computer for HTTPS transport or add the IP addresses
of the remote computers to the TrustedHosts list on the local
computer.

For instructions, see "How to Add a Computer to the TrustedHosts
List" below.


2. Use the Credential parameter in all remote commands.

This is required even when you are submitting the credentials
of the current user.
#>

#Show update status until the amount of installed updates equals the same as the count of updates being reported
sleep -Seconds 30 # Wait for the log file to generate
$dots=80
$dotsCount=0
$lastActivity="";
$installedCount=0;
Write-Host "There is/are $updatesCount pending update(s)`n";

do {
$updatestatus = Get-Content "\\$computer\c$\PSWindowsUpdate.log"
$currentActivity=$updatestatus | select-object -last 1

if (($currentActivity -ne $lastActivity) -AND ($currentActivity -ne $Null)){
Write-Host "Procesing $($installedCount+1) of $updatesCount updates."
Write-Host "`n$currentActivity";
$lastActivity=$currentActivity;
}else{
if ($dotCount++ -le $dots){
Write-Host -NoNewline ".";
if($installedCount -eq $updatesCount){Write-Host "Processing last update: $installedCount of $updatesCount."}
}else{
Write-Host ".";
$dotCount=0;
}
}
sleep -Seconds 10
$ErrorActionPreference = 'SilentlyContinue'
$ErrorActionPreference = 'Continue'
$installedCount = ([regex]::Matches($updatestatus, "Installed")).count
}until ($installedCount -eq $updatesCount)

#restarts the remote computer and waits till it starts up again
if (checkPendingReboot $computer){
"`nReboots required.`nRestarting remote computer $computer to clear pending reboot flags."
Restart-Computer -Wait -ComputerName $computer -Force;
"$computer has been restarted."
}else{"No reboots required."}
}
}until(($updates -eq $null) -OR ($installedCount -eq $updatesCount))

function cleanup{
if(test-netconnection -ComputerName $computer -port 445 -ErrorAction SilentlyContinue){
if(Get-Process -ComputerName $computer powershell -ErrorAction SilentlyContinue){
Write-Host "Terminating any powershell.exe processes."
killProcess -ComputerName $computer -ExecutableName powershell.exe
}
}

if (Test-Path $logFile -ErrorAction SilentlyContinue){
Write-Host "Deleting log to prevent collisions with subsequent runs."
$logFile="\\$computer\c$\PSWindowsUpdate.log"
Write-Host "Removing $logFile."
Remove-item $logFile | Out-Null
}
Write-Host "Removing the Invoke-WuJob's remnant, PSWindowsUpdate scheduled task, from $computer."
invoke-command -computername $computer -Credential $domainAdminCred -ScriptBlock {
if (Get-ScheduledTask -TaskName "PSWindowsUpdate" -ErrorAction SilentlyContinue){
Unregister-ScheduledTask -TaskName PSWindowsUpdate -Confirm:$false} | Out-Null;
}
}
cleanup;

# Revert WSUS registry edits, if any
if($wsus){turnonWsus;}

Write-Host "Windows is now up to date on $computer";
<# Check scheduled task of remote computer in case of simultaneous updating
$task=Invoke-Command -ComputerName $computer -ScriptBlock{(Get-ScheduledTask -TaskName "PSWindowsUpdate").State}
if ($task.State -eq "Running"){"Still Running..."}
#>
}

function updateComputers {

# Experimental
function simultaneousUpdates{
# Start jobs on multiple targets simultaneously
$maxConcurrentJobs=8
ForEach ($server in $remoteComputers) {
"Starting process on $server..."
$running = @(Get-Job | Where-Object { $_.State -eq 'Running' })
if ($running.Count -le $maxConcurrentJobs) {
Start-Job -ScriptBlock{
param($importedFunc,$node);
[ScriptBlock]::Create($importedFunc).Invoke($node);
} -ArgumentList ${function:updateRemoteWindows},$server
}else{
$running | Wait-Job
}
Get-Job | Receive-Job
}

# Update local Windows as soon as remote jobs are completed
$jobsCount = @(Get-Job | Where-Object { $_.State -eq 'Running' }).Count
if ($jobsCount -eq 0) {
updateLocalWindows;
}else{$running | Wait-Job}

<#
# Process all nodes at once
$remoteComputers | %{
# Define what each job does
$script = {
param($Arg0);
invoke-command -computername $Arg0 -scriptBlock{hostname}
#updateRemoteWindowsAutoReboot $server
Start-Sleep 10;
}

# Execute the jobs in parallel
Start-Job $script -ArgumentList $_
}

Get-Job

# Check every X seconds and wait for all jobs to complete
While (Get-Job -State "Running"){Start-Sleep 10;}

# Getting the information back from the jobs. A one-time trigger.
Get-Job | Receive-Job
#>

<# invoke command on a list of servers
Invoke-Command -computername $remoteComputers -ScriptBlock {
param($updateWindows)
[ScriptBlock]::Create($updateWindows).Invoke()
} -Args ${function:applyWindowsUpdates} -ThrottleLimit 1
#>
}

# Validation of prior updates
function validateUpdates{
$remoteComputers|%{invoke-command -computername $_ -Credential $domainAdminCred -scriptblock{
param($importedFunc,$name);
[ScriptBlock]::Create($importedFunc).Invoke($name);
} -Args ${function:listPendingUpdates},$_
}
listPendingUpdates;
}

$warning="This program will forcefully update all computers on the list and automatically reboot them whenever necessary.`n"
$prompt="Please type 'local','remote', 'all','simultaneous' or 'exit' to end"
$warning;
do{
$userInput=Read-Host -Prompt $prompt;
if ($userInput -match "local*"){"Local!";updateLocalWindows;} # -match is faster than -like
if ($userInput -match "remote*"){"Remote!";$remoteComputers|%{updateRemoteWindows -computer $_;};}
if ($userInput -match "all*"){"All!";$remoteComputers|%{updateRemoteWindows $_;};updateLocalWindows;}
if ($userInput -match "simultaneous*"){"Simultaneous!";simultaneousUpdates;}
}while (($userInput -notmatch "(local|remote|all|simultaneous)") -AND ($userInput -notmatch "(end|quit|cancel|exit)"))
validateUpdates;
}

updateComputers;
# Update-Windows-Computers-v0.1.ps1

# Purpose: trigger Microsoft Updates on a list of Windows machines. Reboot each computer when necessary.

# Update this list here via direct text editing or change it to pull a prepared text/csv file
$computerList="SHERVER01","SHERVER02","SHERVER03","SHERVER04"

# Update this list here via direct text editing or change it to pull a prepared text/csv file
#$computerList="SHERVER01","SHERVER02","SHERVER03","SHERVER04"

# Remove this computer from the remote computers list, if exists
$thisComputer=$ENV:computername
$remoteComputers=$computerList|?{$_ -ne $thisComputer}

<#
$currentUser=[Security.Principal.WindowsIdentity]::GetCurrent().Name
$plainTextPassword=Read-Host -Prompt "Input the password for $currentUser"
$password=ConvertTo-SecureString -String $plainTextPassword -AsPlainText -Force
$cred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $currentUser,$password
#>

function updateLocalWindows{
# Prerequisites
function installPrerequisites{
#Import-Module PSWindowsUpdate -force;
#$psWindowsUpdateAvailable=Get-Module PSWindowsUpdate -InformationAction SilentlyContinue;
$psWindowsUpdateAvailable=Get-Module -ListAvailable -Name PSWindowsUpdate -InformationAction SilentlyContinue;
if (!($psWindowsUpdateAvailable)){
try {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12;
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Confirm:$false | Out-Null;
Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted | Out-Null;
Install-Module PSWindowsUpdate -Confirm:$false -Force | Out-Null;
Import-Module PSWindowsUpdate -force | Out-Null;
}
catch{
"Prerequisites not met on $computer.";
}
}
}

function checkPendingReboot{
param([string]$computer=$ENV:computername)

function checkRegistry{
if (Get-ChildItem "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending" -EA Ignore) { return $true }
if (Get-Item "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired" -EA Ignore) { return $true }
if (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" -Name PendingFileRenameOperations -EA Ignore) { return $true }
try {
$util = [wmiclass]"\\.\root\ccm\clientsdk:CCM_ClientUtilities"
$status = $util.DetermineIfRebootPending()
if(($status -ne $null) -and $status.RebootPending){
return $true
}
}catch{}
return $false
}

$localhost=$ENV:computername
if ($localHost -eq $computer){
$result=checkRegistry;
}else{
$result=Invoke-Command -ComputerName $computer -ScriptBlock{
param($importedFunc);
[ScriptBlock]::Create($importedFunc).Invoke();
} -ArgumentList ${function:checkRegistry}
}
return $result;
}
installPrerequisites;

# Register the user of Windows Update Service if it has not been registered
$MicrosoftUpdateID="7971f918-a847-4430-9279-4a52d1efe18d"
$registered=$MicrosoftUpdateID -in (Get-WUServiceManager).ServiceID
if (!($registered)){
Add-WUServiceManager -ServiceID 7971f918-a847-4430-9279-4a52d1efe18d -Confirm:$false
}

# Perform Updates
Get-WindowsUpdate -AcceptAll -MicrosoftUpdate -Install -IgnoreReboot;

if (checkPendingReboot){
$warning="There is a pending reboot flag on this host.`n"
$prompt="Please type 'exit' to cancel or 'reboot' to reboot"
$warning;
do{
$userInput=Read-Host -Prompt $prompt;
if ($userInput -match "reboot"){"Restarting command received!";Restart-Computer;} # -match is faster than -like
}while (($userInput -notmatch "reboot") -AND ($userInput -notmatch "(quit|cancel|exit)"))
}else{"Done."}
}

function listPendingUpdates{
#[DateTime]$last24Hours=(Get-Date).AddHours(-24)
param([string]$computername = $env:COMPUTERNAME)

"Checking computer $computername`:"
$updatesession = [activator]::CreateInstance([type]::GetTypeFromProgID("Microsoft.Update.Session",$computername))
$UpdateSearcher = $updatesession.CreateUpdateSearcher()
$searchresult = $updatesearcher.Search("IsInstalled=0") # 0 = NotInstalled | 1 = Installed

$updates = If ($searchresult.Updates.Count -gt 0) {
$count = $searchresult.Updates.Count
For ($i=0; $i -lt $count; $i++) {
#$timeStamp=[DateTime]$item.LastDeploymentChangeTime
$item = $searchresult.Updates.Item($i)
#if ($timeStamp -gt $last24Hours){
#Create new custom object
[pscustomobject]@{
Title = $item.Title
KB = $item| foreach {$_.KbArticleids}
Severity = $item.MsrcSeverity
IsDownloaded=$item.IsDownloaded
SecurityBulletin = $($item.SecurityBulletinIDs)
Url = $item.MoreInfoUrls
Categories = ($item.Categories | Select-Object -ExpandProperty Name)
BundledUpdates = @($item.BundledUpdates)|ForEach{
[pscustomobject]@{
Title = $_.Title
DownloadUrl = @($_.DownloadContents).DownloadUrl
}

}
}#pscustomobject
#}#if-timeStamp
}#forloop
}#updates

if($updates){$updates |ft kb,title,severity,IsDownloaded -autosize}else{"No pending updates."}
}

function updateRemoteWindows{
<#
.SYNOPSIS
This script will automatically install all avaialable windows updates on a device and will automatically reboot if needed, after reboot, windows updates will continue to run until no more updates are available.
.PARAMETER URL
User the Computer parameter to specify the Computer to remotely install windows updates on.
# Around 40% of this function was forked from https://www.altaro.com/msp-dojo/powershell-windows-updates/
# Partial credit: Luke Orellana
# Note: I've added more features:
# - Check WSUS settings to account for that scenario
# - Prepare the targets by installing prerequisites prior to proceeding further to preemptively resolve dependency errors
# - Include additional dedendencies such as TLS1.2, Nuget & PSGALLERY
# - Check if server needs a reboot before issuing the reboot command instead of just inadvertently trigger reboots
# - Fixed the blank lines in output log causing bug in status query
# - More thorough cleanup routine
# - Detect and handle proxies (in development)
#>

[CmdletBinding()]
param (
[parameter(Mandatory=$true,Position=0)]
[string[]]$computer=$ENV:computername
)

function installPrerequisites{
#Import-Module PSWindowsUpdate -force;
#$psWindowsUpdateAvailable=Get-Module PSWindowsUpdate -InformationAction SilentlyContinue;
$psWindowsUpdateAvailable=Get-Module -ListAvailable -Name PSWindowsUpdate -InformationAction SilentlyContinue;
if (!($psWindowsUpdateAvailable)){
try {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12;
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Confirm:$false | Out-Null;
Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted | Out-Null;
Install-Module PSWindowsUpdate -Confirm:$false -Force | Out-Null;
Import-Module PSWindowsUpdate -force | Out-Null;
}
catch{
"Prerequisites not met on $computer.";
}
}
}

function checkPendingReboot{
param([string]$computer=$ENV:computername)

function checkRegistry{
if (Get-ChildItem "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending" -EA Ignore) { return $true }
if (Get-Item "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired" -EA Ignore) { return $true }
if (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" -Name PendingFileRenameOperations -EA Ignore) { return $true }
try {
$util = [wmiclass]"\\.\root\ccm\clientsdk:CCM_ClientUtilities"
$status = $util.DetermineIfRebootPending()
if(($status -ne $null) -and $status.RebootPending){
return $true
}
}catch{}
return $false
}

$localhost=$ENV:computername
if ($localHost -eq $computer){
$result=checkRegistry;
}else{
$result=Invoke-Command -ComputerName $computer -ScriptBlock{
param($importedFunc);
[ScriptBlock]::Create($importedFunc).Invoke();
} -ArgumentList ${function:checkRegistry}
}
return $result;
}

# Function requires 2 parameters: -computerName and -processName
function killProcess{
[CmdletBinding()]
param(
[string[]]$computerName=$ENV:COMPUTERNAME,
[parameter(Mandatory=$true)][string]$executableName="powershell.exe"
)

# WMI querying method
$processes = Get-WmiObject -Class Win32_Process -ComputerName $ComputerName -Filter "name='$executableName'"

if ($processes){
foreach ($process in $processes) {
$terminationResult = $process.terminate()
$processid = $process.handle

if($terminationResult.returnvalue -eq 0) {
write-host "The process $executableName `($processid`) terminated successfully"
} else {
write-host "The process $executableName `($processid`) termination has some problems"
}
}
}else{
"$executableName is currently not running on $computerName."
}
}

installPrerequisites;

Do{
#starts up a remote powershell session to the computer
do{
$session = New-PSSession -ComputerName $computer #-Credential $cred
"Connecting to remote computer $computer"
sleep -seconds 10
} until ($session.state -match "Opened")

invoke-command -computername $computer -scriptblock{
param($importedFunc);
[ScriptBlock]::Create($importedFunc).Invoke();
} -Args ${function:installPrerequisites};

#retrieves a list of available updates
"Checking for new updates on $computer"
$updates = invoke-command -session $session -scriptblock {Get-wulist #-verbose}

# Count how many updates are available
$updatesCount = ($updates.kb).count

function checkWsus{
# Check if this machine has WSUS settings configured
$wuPath="Registry::HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU";
$wuKey="UseWUServer";
$wuIsOn=(Get-ItemProperty -path $wuPath -name $wuKey -ErrorAction SilentlyContinue).$wuKey;
if($wuIsOn){$GLOBAL:wsus=$True}else{$GLOBAL:wsus=$False}
}

function turnoffWsus{
# Turn WSUS settings OFF temporarily...
$wuPath="Registry::HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU";
$wuKey="UseWUServer";
setRegKey -path $wuPath -name $wuKey -value 0;
restart-service wuauserv;
}

function turnonWsus{
# Turning WSUS settings back to ON status
$wuPath="Registry::HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU";
$wuKey="UseWUServer";
setRegKey -path $wuPath -name $wuKey -value 1;
restart-service wuauserv;
}

# If there are available updates proceed with installing the updates and then reboot the remote machine if required
if ($updates -ne $null){

checkWsus;
if($wsus){turnoffWsus;}
# Create a scheduled task on remote computer name PSWindowsUpdate with this script
<# experimental: currently, this prevented scheduled task from executing
$invokeScript = {
# Check if this machine has WSUS settings configured
$wuPath="Registry::HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU";
$wuKey="UseWUServer";
$wuIsOn=(Get-ItemProperty -path $wuPath -name $wuKey -ErrorAction SilentlyContinue).$wuKey;

if($wuIsOn){
# Perform updates by manipulating WSUS keys
# Turn WSUS settings OFF temporarily...
setRegKey -path $wuPath -name $wuKey -value 0;
restart-service wuauserv;
Import-Module PSWindowsUpdate;
Get-WindowsUpdate -AcceptAll -Install | Out-File C:\PSWindowsUpdate.log;
# Turning WSUS settings back to ON status
setRegKey -path $wuPath -name $wuKey -value 1;
restart-service wuauserv;
}else{
"Triggering Updates...";
Import-Module PSWindowsUpdate;
Get-WindowsUpdate -AcceptAll -Install | Out-File C:\PSWindowsUpdate.log;
(gc C:\PSWindowsUpdate.log) | ? {$_.trim() -ne "" } | set-content C:\PSWindowsUpdate.log;
}
}
#>
#Invoke-WUJob will insert a scheduled task on the remote target as a mean to bypass 2nd hop issues
$invokeScript = {import-module PSWindowsUpdate; Get-WindowsUpdate -AcceptAll -Install | Out-File C:\PSWindowsUpdate.log}
$job=Invoke-WUjob -ComputerName $computer -Script $invokeScript -Confirm:$false -RunNow
#[void]$job;
<# Troubleshooting
ERROR:
Invoke-WUjob : [SERVER] Connecting to remote server SERVER failed with the following error message :
The WinRM client cannot process the request. Default credentials with Negotiate over HTTP can be used only if the
target machine is part of the TrustedHosts list or the Allow implicit credentials for Negotiate option is specified.
For more information, see the about_Remote_Troubleshooting Help topic.
At line:49 char:9
+ Invoke-WUjob -ComputerName $computer -Script $Script -Confirm ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OpenError: (PRDFCL01DC1SV1:String) [Invoke-WUJob], PSRemotingTransportException
+ FullyQualifiedErrorId : ExplicitCredentialsRequired,PSSessionStateBroken


Resolution:
1. Option one is to ignore this error. No actions are required as this error appears to have no effects
on Invoke-WUjob's intended scheduled task creation.
Hence, the remote updating program proceeds as normal.
2. Option two is to fix the targets using this information

-----------------------------------------------------
HOW TO USE AN IP ADDRESS IN A REMOTE COMMAND
-----------------------------------------------------
ERROR: The WinRM client cannot process the request. If the
authentication scheme is different from Kerberos, or if the client
computer is not joined to a domain, then HTTPS transport must be used
or the destination machine must be added to the TrustedHosts
configuration setting.

The ComputerName parameters of the New-PSSession, Enter-PSSession and
Invoke-Command cmdlets accept an IP address as a valid value. However,
because Kerberos authentication does not support IP addresses, NTLM
authentication is used by default whenever you specify an IP address.

When using NTLM authentication, the following procedure is required
for remoting.

1. Configure the computer for HTTPS transport or add the IP addresses
of the remote computers to the TrustedHosts list on the local
computer.

For instructions, see "How to Add a Computer to the TrustedHosts
List" below.


2. Use the Credential parameter in all remote commands.

This is required even when you are submitting the credentials
of the current user.
#>

#Show update status until the amount of installed updates equals the same as the count of updates being reported
sleep -Seconds 30 # Wait for the log file to generate
$dots=80
$dotsCount=0
$lastActivity="";
$installedCount=0;
Write-Host "There is/are $updatesCount pending update(s)`n";

do {
$updatestatus = Get-Content $logFile
$currentActivity=$updatestatus | select-object -last 1
if (($currentActivity -ne $lastActivity) -AND ($currentActivity -isNot $Null)){
Write-Host "Procesing $installedCount of $updatesCount updates."
Write-Host "`n $currentActivity";
$lastActivity=$currentActivity;
}else{
if ($dotCount++ -le $dots){
Write-Host -NoNewline ".";
}else{
Write-Host ".";
$dotCount=0;
}
}
sleep -Seconds 10
$ErrorActionPreference = 'SilentlyContinue'
$ErrorActionPreference = 'Continue'
$installedCount = ([regex]::Matches($updatestatus, "Installed")).count
}until ($installedCount -eq $updatesCount)

#restarts the remote computer and waits till it starts up again
if (checkPendingReboot $computer){
"`nReboots required.`nRestarting remote computer $computer to clear pending reboot flags."
Restart-Computer -Wait -ComputerName $computer -Force
}else{"No reboots required."}
}
}
}until(($updates -eq $null) -OR ($installedCount -eq $updatesCount))

function cleanup{
# Delete log to prevent collisions with subsequent runs
$logFile="\\$computer\c$\PSWindowsUpdate.log"
if (Test-Path $logFile -ErrorAction SilentlyContinue){
Write-Host "Terminating any powershell.exe processes."
killProcess -ComputerName $computer -ExecutableName powershell.exe
Write-Host "Removing $logFile."
Remove-item $logFile | Out-Null

# Cleanup: remove PSWindowsUpdate scheduled task from computer
invoke-command -computername $computer -ScriptBlock {Unregister-ScheduledTask -TaskName PSWindowsUpdate -Confirm:$false} | Out-Null
}
}
cleanup;

# Revert WSUS registry edits, if any
if($wsus){turnonWsus;}

Write-Host "Windows is now up to date on $computer";
<# Check scheduled task of remote computer in case of simultaneous updating
$task=Invoke-Command -ComputerName $computer -ScriptBlock{(Get-ScheduledTask -TaskName "PSWindowsUpdate").State}
if ($task.State -eq "Running"){"Still Running..."}
#>
}

function updateComputers {

function sequentialUpdates{
# Processing updates
$remoteComputers|%{updateRemoteWindows $_;}
updateLocalWindows;
}

function simultaneousUpdates{
# Start jobs on multiple targets simultaneously
$maxConcurrentJobs=8
ForEach ($server in $remoteComputers) {
"Starting process on $server..."
$running = @(Get-Job | Where-Object { $_.State -eq 'Running' })
if ($running.Count -le $maxConcurrentJobs) {
Start-Job -ScriptBlock{
param($importedFunc,$node);
[ScriptBlock]::Create($importedFunc).Invoke($node);
} -ArgumentList ${function:updateRemoteWindows},$server
}else{
$running | Wait-Job
}
Get-Job | Receive-Job
}

# Update local Windows as soon as remote jobs are completed
$jobsCount = @(Get-Job | Where-Object { $_.State -eq 'Running' }).Count
if ($jobsCount -eq 0) {
updateLocalWindows;
}else{$running | Wait-Job}

<#
# Process all nodes at once
$remoteComputers | %{
# Define what each job does
$script = {
param($Arg0);
invoke-command -computername $Arg0 -scriptBlock{hostname}
#updateRemoteWindowsAutoReboot $server
Start-Sleep 10;
}

# Execute the jobs in parallel
Start-Job $script -ArgumentList $_
}

Get-Job

# Check every X seconds and wait for all jobs to complete
While (Get-Job -State "Running"){Start-Sleep 10;}

# Getting the information back from the jobs. A one-time trigger.
Get-Job | Receive-Job
#>

<# invoke command on a list of servers
Invoke-Command -computername $remoteComputers -ScriptBlock {
param($updateWindows)
[ScriptBlock]::Create($updateWindows).Invoke()
} -Args ${function:applyWindowsUpdates} -ThrottleLimit 1
#>
}

# Validation of prior updates
function validateUpdates{
$remoteComputers|%{invoke-command -computername $_ -scriptblock{
param($importedFunc,$name);
[ScriptBlock]::Create($importedFunc).Invoke($name);
} -Args ${function:listPendingUpdates},$_
}
listPendingUpdates;
}

$warning="This program will forcefully update all computers on the list and automatically reboot them whenever necessary.`n"
$prompt="Please type 'exit' to cancel or 'I confirm' to proceed"
$warning;
do{
$userInput=Read-Host -Prompt $prompt;
if ($userInput -match "i confirm*"){"Bingo!";sequentialUpdates;} # -match is faster than -like
if ($userInput -match "simultaneous*"){"Simultaneous!";simultaneousUpdates;} # -match is faster than -like
}while (($userInput -notmatch "(i confirm|simultaneous)") -AND ($userInput -notmatch "(quit|cancel|exit)"))
validateUpdates;
}

updateComputers;

Leave a Reply

Your email address will not be published. Required fields are marked *