Version 3:
# updateWindowsList.ps1
# Version 0.0.3
#
# Features:
# - New feature over version 0.0.1: Simultaneous Executions
# - New feature over version 0.0.2: Handle cases where target hosts have no Internet access
#
# Requirements:
# - WinRM must be enabled on target computer(s)
# User input variables
$computernames='testWindows'
$directMicrosoftUpdates=$true
$csvFile='C:\updateResults.csv'
# Domain Admin credentials (to be injected by Jenkins)
$adminUsername='domain\admin'
$plaintextPassword='PASSWORD'
$encryptedPassword=ConvertTo-securestring $plaintextPassword -AsPlainText -Force
$adminCredential=New-Object -TypeName System.Management.Automation.PSCredential -Args $adminUsername,$encryptedPassword
function invokeWindowsUpdate{
[CmdletBinding()]
param (
[parameter(Mandatory=$true,Position=0)][string]$computer,
[parameter(Mandatory=$false,Position=1)][System.Management.Automation.PSCredential]$adminCredential,
[parameter(Mandatory=$false,Position=2)][bool]$microsoftUpdates=$false,
[parameter(Mandatory=$false,Position=3)][bool]$bypassWsus=$false,
[parameter(Mandatory=$false,Position=4)][int]$winRmPort=5985,
[parameter(Mandatory=$false,Position=5)][string]$logFile='C:\PSWindowsUpdate.log'
)
$ErrorActionPreference='stop'
<#
.SYNOPSIS
This script will automatically install all avaialable windows updates on a device and will automatically reboot if needed.
After reboots, windows updates will continue to run until all updates are installed.
# Features:
# - Check WSUS settings (bypass if required by the boolean value)
# - 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
# Future developments:
# - Detect and handle proxies
#>
function installPsWindowsUpdate{
$ErrorActionPreference='stop'
$psWindowsUpdateAvailable=Get-Module -ListAvailable -Name PSWindowsUpdate -EA SilentlyContinue;
if (!($psWindowsUpdateAvailable)){
try {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
if(Get-PackageProvider 'nuget' -ea SilentlyContinue -Force){
Install-PackageProvider -Name Nuget -RequiredVersion 2.8.5.201 -Force
}
if((Get-PSRepository psgallery).InstallationPolicy -ne 'Trusted'){
$null=Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted
}
$psWindowsUpdate=Get-Module -ListAvailable -Name 'PSWindowsUpdate' -EA Ignore
if(!$psWindowsUpdate){
$null=Install-Module PSWindowsUpdate -Confirm:$false -Force
}
# Register Microsoft Update Service if it has not been registered
$null=Import-Module PSWindowsUpdate -force
$microsoftUpdateId='7971f918-a847-4430-9279-4a52d1efe18d'
if (!($microsoftUpdateId -in (Get-WUServiceManager).ServiceID)){
Add-WUServiceManager -ServiceID $microsoftUpdateId -Confirm:$false
}
return $true;
}
catch{
write-host "Prerequisites not met on $ENV:COMPUTERNAME.";
return $false;
}
}else{
# Register Microsoft Update Service if it has not been registered
$microsoftUpdateId='7971f918-a847-4430-9279-4a52d1efe18d'
if (!($microsoftUpdateId -in (Get-WUServiceManager).ServiceID)){
Add-WUServiceManager -ServiceID $microsoftUpdateId -Confirm:$false
}
return $true
}
}
function checkPendingReboot([string]$computer=$ENV:computername,$session){
function checkRegistry{
if (Get-ChildItem "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending" -EA Ignore) { return $true }
if (Get-ChildItem "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootInProgress" -EA Ignore) { return $true }
if (Get-Item "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired" -EA Ignore) { return $true }
if (Get-Item "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\PackagesPending" -EA Ignore) { return $true }
if (Get-Item "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\PostRebootReporting" -EA Ignore) { return $true }
if (Get-Item 'HKLM:\SOFTWARE\Microsoft\ServerManager\CurrentRebootAttemps' -EA Ignore) { return $true }
if (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" -Name 'PendingFileRenameOperations' -EA Ignore) { return $true }
if (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" -Name 'PendingFileRenameOperations2' -EA Ignore) { return $true }
if (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce' -Name 'DVDRebootSignal' -EA Ignore) { return $true }
if (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon' -Name 'JoinDomain' -EA Ignore) { return $true }
if (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon' -Name 'AvoidSpnSet' -EA Ignore) { return $true }
try{ # This peruses CCM utility, if exists
$util = [wmiclass]"\\.\root\ccm\clientsdk:CCM_ClientUtilities"
$status = $util.DetermineIfRebootPending()
if(($null -ne $status) -and $status.RebootPending){
$result.SCCMRebootPending = $true
}
}catch{
return $false
}
}
if ($ENV:computername -eq $computer){
$result=checkRegistry;
}else{
$result=Invoke-Command -session $session -ScriptBlock{
param($importedFunc);
[ScriptBlock]::Create($importedFunc).Invoke();
} -ArgumentList ${function:checkRegistry}
}
return $result;
}
function clearRebootFlags($computer,$session,$verbose=$false){
if (checkPendingReboot $computer $session){
$isLocalHost=$env:computername -eq $computer
if($isLocalHost){
write-warning "$computer is LOCALHOST. Please reboot manually to clear reboot flags"
return $false
}else{
write-host "`nRestarting remote computer $computer to clear pending reboot flags..."
if($adminCredential){
Restart-Computer -Wait -ComputerName $computer -Credential $adminCredential -Force
}else{
Restart-Computer -Wait -ComputerName $computer -Force
}
write-host "$computer has been successfully restarted!" -ForegroundColor Yellow
return $true
}
}else{
if($verbose){write-host "There are no pending reboot flags to clear." -ForegroundColor Green}
return $true
}
}
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}
return $wuIsOn
}
function turnoffWsus{
# Turn WSUS settings OFF temporarily...
$wuPath="Registry::HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU";
$wuKey="UseWUServer";
Set-Itemproperty -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";
Set-Itemproperty -path $wuPath -Name $wuKey -value 1
restart-service wuauserv;
}
function connectWinRm($computer,$adminCredential,$winRmPort=5985){
if(!$computer){
write-warning "Computer name must be specified to initiate a WinRM connection."
return $false
}
# Legacy equivalent to Test-Netconnection
function checkNetConnection($computername,$port,$timeout=1000,$verbose=$false) {
$tcp = New-Object System.Net.Sockets.TcpClient;
try {
$connect=$tcp.BeginConnect($computername,$port,$null,$null)
$wait = $connect.AsyncWaitHandle.WaitOne($timeout,$false)
if(!$wait){
$null=$tcp.EndConnect($connect)
$tcp.Close()
if($verbose){
Write-Host "Connection Timeout" -ForegroundColor Red
}
Return $false
}else{
$error.Clear()
$null=$tcp.EndConnect($connect) # Dispose of the connection to release memory
if(!$?){
if($verbose){
write-host $error[0].Exception.Message -ForegroundColor Red
}
$tcp.Close()
return $false
}
$tcp.Close()
Return $true
}
} catch {
return $false
}
}
function enableWinRmRemotely($remoteComputer,$winRmPort,$adminCredential){
function Check-NetConnection($computername,$port,$timeout=1000,$verbose=$false) {
$tcp = New-Object System.Net.Sockets.TcpClient;
try {
$connect=$tcp.BeginConnect($computername,$port,$null,$null)
$wait = $connect.AsyncWaitHandle.WaitOne($timeout,$false)
if(!$wait){
$null=$tcp.EndConnect($connect)
$tcp.Close()
if($verbose){
Write-Host "Connection Timeout" -ForegroundColor Red
}
Return $false
}else{
$error.Clear()
$null=$tcp.EndConnect($connect) # Dispose of the connection to release memory
if(!$?){
if($verbose){
write-host $error[0].Exception.Message -ForegroundColor Red
}
$tcp.Close()
return $false
}
$tcp.Close()
Return $true
}
} catch {
return $false
}
}
if (!(get-command psexec)){
# Install Chocolatey
if (!(Get-Command choco.exe -ErrorAction SilentlyContinue)) {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12;
Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
}
choco install sysinternals -y;
}
$success=check-netconnection $remoteComputer $winRmPort
write-host 'Attempting to use psexec to enable WinRM remotely...'
if(!$adminCredential){ # Enable WinRM Remotely
$null=psexec.exe \\$remoteComputer -s C:\Windows\system32\winrm.cmd qc -quiet;
}else{
$username=$adminCredential.Username
$password=$adminCredential.GetNetworkCredential().Password
$null=psexec.exe \\$remoteComputer -u $username -p $password -s C:\Windows\system32\winrm.cmd qc -quiet
}
return check-netconnection $remoteComputer $winRmPort
}
# If machine is not pingable, wait five minutes
$fiveMinuteTimer=[System.Diagnostics.Stopwatch]::StartNew()
do{
$ping = Test-Connection $computer -quiet
if($ping -eq $false){sleep 1}
$pastFiveMinutes=$fiveMinuteTimer.Elapsed.TotalMinutes -ge 5
}until ($ping -eq $true -or $pastFiveMinutes)
$fiveMinuteTimer.stop()
$winRmAvailable=checkNetConnection $computer $winRmPort
if(!$winRmAvailable){
write-host "Attempting to enable WinRM on $computer" -ForegroundColor Yellow
$enableWinRmSuccessful=enableWinRmRemotely $computer
if($enableWinRmSuccessful){
write-host "WinRM enabled: $enableWinRmSuccessful"
}else{
write-warning "WinRM could not be enabled remotely. WinRM connection aborted."
return $false
}
}
# Wait for WinRm session prior to proceeding
if($session.state -eq 'Opened'){remove-pssession $session}
do{
$session=if($adminCredential){
try{
New-PSSession -ComputerName $computer -Credential $adminCredential -ea Stop
}catch{
New-PSSession -ComputerName $computer -Credential $adminCredential -SessionOption $(new-pssessionoption -IncludePortInSPN)
}
}else{
try{
New-PSSession -ComputerName $computer -ea Stop
}catch{
New-PSSession -ComputerName $computer -SessionOption $(new-pssessionoption -IncludePortInSPN)
}
}
sleep -seconds 1
if ($session){
write-host "Connected to $computer."
return $session
}
} until ($session.state -match "Opened")
}
function includePsWindowsUpdate($session){
$psWindowsUpdateIsAvailable=invoke-command -session $session -scriptblock {
param($installPsWindowsUpdate);
# Register Microsoft Update Service if it has not been registered
return [ScriptBlock]::Create($installPsWindowsUpdate).Invoke()
} -Args ${function:installPsWindowsUpdate}
if($psWindowsUpdateIsAvailable){
return $true
}else{
return $false
}
}
function cleanupWuJob($session,$logFile='C:\PSWindowsUpdate.log'){
invoke-command -Session $session -ScriptBlock {
param($logFile)
if (Get-ScheduledTask -TaskName "PSWindowsUpdate" -ErrorAction SilentlyContinue){
Write-Host "Removing PSWindowsUpdate scheduled task from $env:computername..."
Unregister-ScheduledTask -TaskName 'PSWindowsUpdate' -Confirm:$false};
if (Test-Path $logFile -ErrorAction SilentlyContinue){
Write-Host "Removing $logFile..."
Remove-item $logFile -force
}
} -Args $logFile
}
write-warning "$computer will go through Windows Update and will REBOOT AUTOMATICALLY (if necessary).`r`nPress Ctrl+C at anytime to cancel."
$session=connectWinRm $computer $adminCredential
$targetHasInternet=invoke-command -ComputerName $computer -Credential $adminCredential -scriptblock{test-connection 8.8.8.8 -Count 1 -Quiet}
if(!$targetHasInternet){
write-host "$computer`: no internet connectivity detected"
function updateLocalWindowsUsingComObjects($autoReboot=$false,$logfile='c:\WindowsUpdate.log'){
# in case contents of function is called from scheduled tasks without any default argument values
$logFile=if($logFile){$logFile}else{'c:\WindowsUpdate.log'}
if(!(test-path $logfile)){
new-item $logfile -type file -force
}
$status="$(get-date) => $env:computername patching STARTED"
write-host $status
Add-Content -Path $logfile -Value $status
$updateSession=New-Object -Com Microsoft.Update.Session
$updateSearcher=$updateSession.CreateUpdateSearcher()
$searchCriteria="IsInstalled=0 AND Type='Software'" # "IsHidden=0 AND IsInstalled=0 AND Type='Software'"
$availableUpdates=$updateSearcher.Search($searchCriteria)
$availableUpdatesCount=$availableUpdates.Updates.count
if($availableUpdatesCount -eq 0){
$status="$(get-date) => $env:computername has 0 available updates. Patching FINISHED"
write-host $status
Add-Content -Path $logfile -Value $status
return $true
}else{
$status="$(get-date) => $availableUpdatesCount available updates detected"
write-host $status
Add-Content -Path $logfile -Value $status
$updatesToDownload=New-Object -Com Microsoft.Update.UpdateColl
For ($i=0; $i -lt $availableUpdates.Updates.Count; $i++){
$item = $availableUpdates.Updates.Item($i)
$Null = $updatesToDownload.Add($item)
}
$updateSession=New-Object -Com Microsoft.Update.Session
$downloader=$updateSession.CreateUpdateDownloader()
$downloader.Updates=$updatesToDownload
try{
$null=$downloader.Download()
}catch{
write-warning $_
}
$downloadedUpdates=New-Object -Com Microsoft.Update.UpdateColl
For ($i=0; $i -lt $availableUpdates.Updates.Count; $i++){
$item = $availableUpdates.Updates.Item($i)
If ($item.IsDownloaded) {
$null=$downloadedUpdates.Add($item)
}
}
$downloadedCount=$downloadedUpdates.count
if($downloadedCount -eq 0){
$status="$(get-date) => Installation cannot proceed 0 successful downloads."
write-host $status
Add-Content -Path $logfile -Value $status
return $false
}else{
$status="$(get-date) => $downloadedCount of $availableUpdatesCount updates downloaded successfully"
write-host $status
Add-Content -Path $logfile -Value $status
$installCount=0
foreach($update in $downloadedUpdates){
$thisUpdate = New-object -com "Microsoft.Update.UpdateColl"
$thisCount=$installCount++ +1
$null=$thisUpdate.Add($update)
$installer = $updateSession.CreateUpdateInstaller()
$installer.Updates = $thisUpdate
$installResult = $installer.Install()
$translatedCode=switch ($installResult.ResultCode){
0 {'not started'}
1 {'in progress'}
2 {'succeeded'}
3 {'succeeded with errors'}
4 {'failed'}
5 {'aborted'}
}
$status="$(get-date) => $thisCount of $downloadedCount $translatedCode`: $($update.Title)"
write-host $status
Add-Content -Path $logfile -Value $status
}
}
}
$pendingReboot=.{
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 (($null -ne $status) -and $status.RebootPending) {
return $true
}
}catch{}
return $false
}
If($pendingReboot){
if($autoReboot){
$status="$(get-date) => $env:computername REBOOTED"
write-host $status
Add-Content -Path $logfile -Value $status
(Get-WMIObject -Class Win32_OperatingSystem).Reboot()
}else{
$status="$(get-date) => $env:computername required a REBOOT"
write-host $status
Add-Content -Path $logfile -Value $status
}
}else{
$status="$(get-date) => $env:computername patching FINISHED"
write-host $status
Add-Content -Path $logfile -Value $status
}
}
function invokeScheduledTaskCallPowerShellFunction{
param(
[string[]]$computernames,
[string]$scriptblock,
[string]$taskName,
[string]$description,
[string]$repeatIntervalMinutes,
[System.Management.Automation.PSCredential]$credential
)
$username=$credential.Username;
$plaintextPassword=$credential.GetNetworkCredential().password;
if ($credential){
foreach ($computer in $computernames){
write-host "Adding task on $computer..."
$thisSession=if($credential){
try{
New-PSSession -ComputerName $computer -Credential $credential -ea Stop
}catch{
New-PSSession -ComputerName $computer -Credential $credential -SessionOption $(new-pssessionoption -IncludePortInSPN)
}
}else{
try{
New-PSSession -ComputerName $computer -ea Stop
}catch{
New-PSSession -ComputerName $computer -SessionOption $(new-pssessionoption -IncludePortInSPN)
}
}
if ($thisSession.state -match "Opened"){
$scheduledTaskAdded=Invoke-Command -session $thisSession -ScriptBlock{
param($scriptblock,$taskName,$description,$repeatMinutes,$user,$password)
$windowsVersion=[Environment]::OSVersion.Version
$windowsUpdateScript='C:\Scripts\WindowsUpdates.ps1'
if(!(test-path $windowsUpdateScript)){
$null=new-item $windowsUpdateScript -type File -force
$null=Unblock-File -Path $windowsUpdateScript
}
$null=Set-Content -Path $windowsUpdateScript -Value $scriptblock
if($windowsVersion -ge [version]'6.2'){
$username=if($user -notmatch '\\'){"$env:USERDOMAIN`\$user"}else{$user}
#$encryptedPassword=ConvertTo-SecureString $password -AsPlainText -Force
#$adminCredential=New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $username,$encryptedPassword;
# Unrestrict this Domain Administrator from security prompts
Set-Executionpolicy -Scope CurrentUser -ExecutionPolicy UnRestricted -Force
$settingsCommand = New-ScheduledTaskSettingsSet -MultipleInstances IgnoreNew -ExecutionTimeLimit 0
$callPowerShell = New-ScheduledTaskAction -Execute "Powershell.exe" -Argument "-ExecutionPolicy Bypass $windowsUpdateScript"
$runNow=$false
$taskTrigger = if($repeatMinutes -gt 0){
New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes $repeatMinutes)
}else{
$runNow=$true;
New-ScheduledTaskTrigger -Once -At (get-date).AddSeconds(-1)
}
# Unregister the Scheduled task if it already exists
Get-ScheduledTask $taskName -ErrorAction SilentlyContinue | Unregister-ScheduledTask -Confirm:$false;
# Create new scheduled task
$null=Register-ScheduledTask -Action $callPowerShell -Trigger $taskTrigger `
-TaskName $taskName -Description $description `
-User $username -Password $password `
-Settings $settingsCommand -RunLevel Highest;
if($runNow){
Start-ScheduledTask -TaskName $taskname
$timeout = 60 ## seconds
$timer = [Diagnostics.Stopwatch]::StartNew()
while (((Get-ScheduledTask -TaskName $taskname).State -ne 'Running') -and ($timer.Elapsed.TotalSeconds -lt $timeout)) {
Write-Verbose -Message "Waiting on scheduled task..."
Start-Sleep -Seconds 2
}
Write-host "$taskname has taken $([math]::round(($timer.Elapsed.TotalSeconds),2)) seconds to initiate"
$timer.Stop()
start-sleep -seconds 5
}
return $true
}else{
write-host "$env:computername is too old. Just turn it off."
return $false
}
} -ArgumentList $scriptblock,$taskName,$description,$repeatIntervalMinutes,$username,$plainTextPassword
Remove-PSSession $thisSession
return $scheduledTaskAdded
}
}
}else{
write-host "Please run this program with a valid Administrator account."
return $false
}
}
$taskName='windowsUpdate'
$taskDescription='Perform Windows Updates'
$taskAdded=invokeScheduledTaskCallPowerShellFunction `
-computernames $computer `
-scriptblock $function:updateLocalWindowsUsingComObjects `
-taskName $taskName `
-description $taskDescription `
-repeatIntervalMinutes 0 `
-credential $adminCredential
if($taskAdded){
$updatesLog="\\$computer\c$\WindowsUpdate.log"
$localUpdatesLog='c:\WindowsUpdate.log'
$logContent=$previousLine=''
$updatesCompleted=$false
write-host "Checking $updatesLog for Windows Update statuses..."
while(!$updatesCompleted){
# Testing variance in results of local vs invoke-command
# $computer='testWindows'
# $updatesLog="\\$computer\c$\windows\system32\drivers\etc\hosts"
# $localUpdatesLog='c:\windows\system32\drivers\etc\hosts'
# $x=get-content $updatesLog
# $y=invoke-command -ComputerName $computer -ScriptBlock{
# param($localUpdatesLog)
# get-content $localUpdatesLog
# } -Args $localUpdatesLog
$logContent=try{
get-content $updatesLog -credential $adminCredential -ea Stop
}catch{
invoke-command -ComputerName $computer -Credential $adminCredential -ScriptBlock{
param($localUpdatesLog)
get-content $localUpdatesLog
} -Args $localUpdatesLog
}
if($logContent){
$currentLine=($logContent|select -last 1|out-string).trim()
$updatesCompleted=$currentLine -match 'FINISHED$'
$rebootRequired=$currentLine -match 'REBOOT$'
if($currentLine -ne $previousLine){
write-host "`r`n$currentLine"
$previousLine=$currentLine
}else{
write-host '.' -nonewline
sleep 10
}
if($rebootRequired){
$null=try{
Restart-Computer -Force -ComputerName $computer -credential $adminCredential -Wait -ea Stop
$currentLine="$(get-date) => $computer REBOOTED"
}catch{
invoke-command -ComputerName $computer -Credential $adminCredential -ScriptBlock{
write-host "Restarting $env:computername..."
Restart-Computer -Force
}
$computerIsOff=$computerIsOn=$computerRestarted=$False
do{
if(!$computerIsOff){
write-host "." -NoNewline
sleep 1
$computerIsOff=!(test-connection $computer -Count 1 -Quiet)
}elseif(!$computerIsOn){
write-host "." -NoNewline
sleep 1
$computerIsOn=test-connection $computer -Count 1 -Quiet
if($computerIsOn){
$startUpTime=invoke-command -computername $computer -Credential $adminCredential -scriptblock{(Get-CimInstance -ClassName win32_OperatingSystem).lastbootuptime}
$computerRestarted=$True
$currentLine="$(($startupTime|out-string).trim()) => $computer REBOOTED"
}
}
}until($computerRestarted)
}
try{
Add-content $updatesLog $currentLine -credential $adminCredential
}catch{
invoke-command -ComputerName $computer -Credential $adminCredential -ScriptBlock{
param($localUpdatesLog,$currentLine)
Add-content $localUpdatesLog $currentLine
} -Args $localUpdatesLog,$currentLine
}
write-host $currentLine
do{
$newSession=if($adminCredential){
try{
New-PSSession -ComputerName $computer -Credential $adminCredential -ea Stop
}catch{
New-PSSession -ComputerName $computer -Credential $adminCredential -SessionOption $(new-pssessionoption -IncludePortInSPN)
}
}else{
try{
New-PSSession -ComputerName $computer -ea Stop
}catch{
New-PSSession -ComputerName $computer -SessionOption $(new-pssessionoption -IncludePortInSPN)
}
}
sleep 1
}until($newSession.State -eq 'Opened')
$currentLine="$(get-date) => $taskName re-triggered"
invoke-command -session $newSession -scriptblock {
param ($localUpdatesLog,$taskName,$currentLine)
Start-ScheduledTask -TaskName $taskname
$null=Add-content $localUpdatesLog $currentLine
} -Args $localUpdatesLog,$taskName,$currentLine
Remove-PSSession $newSession
write-host $currentLine
}elseif($updatesCompleted){
$currentLine="$(get-date) => $computer patching FINISHED"
$null=try{
Add-content $updatesLog $currentLine -ea Stop
}catch{
invoke-command -ComputerName $computer -Credential $adminCredential -ScriptBlock{
param($localUpdatesLog,$currentLine)
Add-content $localUpdatesLog $currentLine
} -Args $localUpdatesLog,$currentLine
}
write-host $currentLine -ForegroundColor Green
}
}else{
write-host '.' -nonewline
sleep 10
}
}
}
}else{
if(!(includePsWindowsUpdate $session)){
remove-pssession $session
return $false
}
if($bypassWsus){
$wsusIsOn=Invoke-Command -session $session -scriptblock{
param($checkWsus);
[scriptblock]::create($checkWsus).invoke()
} -Args ${function:checkWsus}
if($wsusIsOn){
Invoke-Command -session $session -scriptblock{
param($turnoffWsus);
[scriptblock]::create($turnoffWsus).invoke()
} -args ${function:turnoffWsus}
}
}
Do{
#retrieves a list of available updates
write-host "Checking for new updates on $computer..."
$updates=invoke-command -session $session -scriptblock {
param($microsoftUpdates)
$executionPolicy=Get-ExecutionPolicy
if($executionPolicy -notmatch 'RemoteSigned|Unrestricted'){
Set-ExecutionPolicy RemoteSigned -force
}
$null=Import-Module PSWindowsUpdate
$availableUpdates=if($microsoftUpdates){
Get-wulist -MicrosoftUpdate -verbose
}else{
Get-Wulist -verbose
}
write-host $($availableUpdates|out-string).trim()
Set-ExecutionPolicy $executionPolicy -force
return $availableUpdates
} -Args $microsoftUpdates
$updatesCount=$updates.KB.count # $updates.count returns $null when count equals 1
# If there are available updates, proceed with installing the updates and then reboot the remote machine if required
if ($updatesCount){
#Invoke-WUJob will insert a scheduled task on the remote target as a mean to bypass 2nd hop issues
invoke-command -Session $session {
param($microsoftUpdates)
$logFile='C:\PSWindowsUpdate.log'
if(test-path $logFile){remove-item $logFile -force}
if($microsoftUpdates){
$invokeScript={
import-module PSWindowsUpdate;
Get-WindowsUpdate -AcceptAll -MicrosoftUpdate -Install | Out-File 'C:\PSWindowsUpdate.log'
}
}else{
$invokeScript={
import-module PSWindowsUpdate;
Get-WindowsUpdate -AcceptAll -Install | Out-File 'C:\PSWindowsUpdate.log'
}
}
Invoke-WUjob -ComputerName $env:computername -Script $invokeScript -Confirm:$false -RunNow
write-host "Windows Updates have been triggerred. Now checking for its log file...`r`n"
Do{
$logFileGenerated=if(test-path $logFile){get-content $logFile}else{$false}
if(!$logFileGenerated){
Start-Sleep -Seconds 1
write-host '.' -NoNewline
}
}until($logFileGenerated)
} -Args $microsoftUpdates
#Show update status until the amount of installed updates equals the same as the count of updates being reported
$dotLimit=10
$dotCount=0
$minute=0
$lastActivity=$null;
$installedCount=0;
Write-Host "`r`nThere $(if($updatesCount -eq 1){'is'}else{'are'}) $updatesCount pending update(s).";
do {
$getWindowsUpdateLog={param($logFile);Get-Content $logFile}
$updatestatus=Invoke-Command -Session $session -ScriptBlock $getWindowsUpdateLog -Args $logFile
$currentActivity=.{
$index=$updatestatus.count-1
do{
$value=$updatestatus[$index--]
}until($value)
return $value
}
$installedCount=([regex]::Matches($updatestatus, "Installed")).count
$updatesCompleted=$installedCount -eq $updatesCount
$failuresCount=([regex]::Matches($updatestatus, "Failed")).count
if ($currentActivity -ne $lastActivity){
if($currentActivity -match 'Installed'){
Write-Host "`r`nSuccessfully installed $installedCount of $updatesCount updates: $currentActivity" -ForegroundColor Green
}elseif($currentActivity -match 'Failed'){
Write-Host "`r`nFailed item: $currentActivity" -ForegroundColor Yellow
}else{
Write-Host "`r`nCurrent activity: $currentActivity"
}
$lastActivity=$currentActivity;
}else{
if ($dotCount++ -le $dotLimit){
Write-Host -NoNewline "."
}else{
$minute++
Write-Host "`r`nMinute count: $minute"
$dotCount=0
}
}
Start-Sleep -Seconds 6
}until ($updatesCompleted -or $failuresCount -ge 2)
}else{
write-host "There are $updatesCount pending updates detected."
}
if(checkPendingReboot $computer $session){
$localhostRebootFlag=!(clearRebootFlags $computer $session)
}else{
write-host "No pending reboots detected on $computer"
}
if($session.State -ne 'Opened'){
write-host "Reconnecting to $computer..." -ForegroundColor Yellow
$session=connectWinRm $computer $adminCredential
}
cleanupWuJob $session $logFile
if($updatesCompleted -or !$updatesCount){
Write-Host "`r`nWindows is now up to date on $computer" -ForegroundColor Green
}
}until(!$updatesCount -or $updatesCompleted -or $localhostRebootFlag)
if($bypassWsus -and $wsusIsOn -and $targetHasInternet){
write-host "Reverting WSUS registry edits"
Invoke-Command -session $session -scriptblock{
param($turnonWsus);
[scriptblock]::create($turnonWsus).invoke()
} -args ${function:turnonWsus} ;
}
$localhostRebootFlag=!(clearRebootFlags $computer $session)
if($session.State -eq 'Opened'){Remove-PSSession $session}
}
if($localhostRebootFlag){
write-host "$computer requires a reboot to complete the updates"
return $false
}else{
return $true
}
}
function confirmation($content,$testValue="I confirm",$maxAttempts=3){
$confirmed=$false;
$attempts=0;
$content|write-host
write-host "Please review this content for accuracy.`r`n"
while ($attempts -le $maxAttempts){
if($attempts++ -ge $maxAttempts){
write-host "A maximum number of attempts have reached. No confirmations received!`r`n"
break;
}
$userInput = Read-Host -Prompt "Please type in this value => $testValue <= to confirm. Input CANCEL to skip this item";
if ($userInput.ToLower() -eq $testValue.ToLower()){
$confirmed=$true;
write-host "Confirmed!`r`n";
break;
}elseif($userInput -like 'cancel'){
write-host 'Cancel command received.'
$confirmed=$false
break
}else{
Clear-Host;
$content|write-host
write-host "Attempt number $attempts of $maxAttempts`: $userInput does not match $testValue. Try again or Input CANCEL to skip this item`r`n"
}
}
return $confirmed;
}
function obtainDomainAdminCredentials{
# Legacy domain binding function
function isValidCred($u,$p){
# Get current domain using logged-on user's credentials
$domain = "LDAP://" + ([ADSI]"").distinguishedName
$domainCred = New-Object System.DirectoryServices.DirectoryEntry($domain,$u,$p)
if ($domainCred){
return $True
}else{
return $False
}
}
function isDomainAdmin($username){
if (!(get-module activedirectory)){Install-WindowsFeature RSAT-AD-PowerShell -Confirm:$false}
if((Get-ADUser $username -Properties MemberOf).MemberOf -match '^CN=Domain Admins'){
return $True;
}else{return $false;}
}
# Create a domain context
function dotNetDomainBind{
Add-Type -AssemblyName System.DirectoryServices.AccountManagement;
Try {
$type = [System.DirectoryServices.AccountManagement.ContextType]::Domain;
$context = New-Object System.DirectoryServices.AccountManagement.PrincipalContext $type,$env:USERDOMAIN;
return $context;
} Catch {
If ($_.Exception.InnerException -like "*The server could not be contacted*") {
write-host "Could not contact a server for the specified domain $env:USERDOMAIN via DotNet Method.";
} Else {
write-host "Unknown errors occured while attempting to contact $env:USERDOMAIN via DotNet Method.";
}
return $false;
}
}
$attempt=0;
$maxAttempts=3;
$plainTextPassword='';
Do{
$attempt++;
$failureMessage = $null;
[string][ValidateNotNullOrEmpty()]$userName=Read-Host -Prompt "Please input a User ID";
if($userName -notmatch '\\'){
$username=$env:USERDOMAIN+'\'+$userName
}
$password = Read-Host -Prompt "Please type in the password for user $userName" -AsSecureString;
$plainTextPassword=[Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($password))
# Test bind to this credential
try {
# Test bind
$context=dotNetDomainBind
if($context){
$validatedAccount = $context.ValidateCredentials($userName,$plainTextPassword)
}else{
$validatedAccount=isValidCred -u $userName -p $plainTextPassword
}
If ($validatedAccount) {
$onlyUserName=$userName -replace '.+\\'
$isDomainAdmin=isDomainAdmin -username $onlyUserName;
if($isDomainAdmin){
$credentials = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $userName,$password;
$validAccount=$true;
}else{
$failureMessage += "$attempt out of $maxAttempts`: $userName account is valid, but it is not a Domain Admin";
}
}else{
$failureMessage += "$attempt out of $maxAttempts`: username and/or password error";
}
}catch{
write-warning $_
$failureMessage += "Unable to bind to $env:USERDNSDOMAIN.";
}
# Depending on whether there are failures, proceed accordingly
if($failureMessage){
If ($attempt -lt $maxAttempts-1) {
$message = "$failureMessage`: Authentication error. Please Try again.";
Write-Warning $message;
}elseif($attempt -eq $maxAttempts-1){
$message = "$failureMessage`: Last attempt.";
Write-Warning $message;
$credentials= $false;
}
}
} Until (($ValidAccount) -or ($Attempt -ge $MaxAttempts))
if($credentials){return $credentials;}else{return $false}
}
function updateWindows($computerNames,$adminCredential,$microsoftUpdates=$false,$csvFile='C:\updateResults.csv'){
get-job|stop-job|remove-job -force
foreach ($computerName in $computerNames){
Start-Job -name $computerName -ScriptBlock {
param($invokeWindowsUpdate,$computerName,$adminCredential,$microsoftUpdates)
[scriptblock]::create($invokeWindowsUpdate).invoke($computerName,$adminCredential,$microsoftUpdates)
} -Args ${function:invokeWindowsUpdate},$computerName,$adminCredential,$microsoftUpdates
}
$lineBreak=30
$dotCount=0
$minute=0
write-host "Minute`r`n$minute`:" -NoNewline -ForegroundColor Yellow
$jobResults=@()
$jobsCount=(get-job).count
do{
$completedJobs=get-job|?{$_.State -eq 'Completed'}
foreach ($job in $completedJobs){
$computer=$job.Name
write-host "`r`n===================================================`r`n$computer job completed with these messages:`r`n===================================================`r`n"
$timeStamp=$job.PSBeginTime
$minutesElapsed=[math]::round(($job.PSEndTime-$job.PSBeginTime).TotalMinutes,2)
$computer=$job.Name
$completed=receive-job -id $job.id
$result=[pscustomobject][ordered]@{
computerName=$computer
timeStamp=$timeStamp
minutesElapsed=$minutesElapsed
completed=$completed
}
$jobResults+=,$result
remove-job -id $job.id
}
if($dotCount++ -lt $lineBreak){
write-host '.' -NoNewline
}else{
$minute++
write-host "`r`n$minute`t:" -ForegroundColor Yellow -NoNewline
$dotCount=0
}
Start-Sleep -seconds 2
}until($jobsCount -eq $jobResults.count -or !(get-job))
write-host "`r`n$(($jobResults|ft|out-string).trim())"
$jobResults | Export-Csv -Path $csvFile -NoTypeInformation -Append
write-host "Windows Update results have been exported to: $csvFile"
$localMachineIsPendingReboot=($jobResults|?{$_.computername -eq $env:computername}).pendingReboot -eq $true
if($localMachineIsPendingReboot){
$confirmed=confirmation "I want to reboot this local machine $env:computername"
if($confirmed){
Restart-Computer $env:computerName -force
}else{
write-warning "Please reboot $env:computername manually to complete its updates."
}
}else{
write-host 'Done.' -ForegroundColor Green
}
}
if(!$adminCredential){$adminCredential=obtainDomainAdminCredentials}
updateWindows $computerNames $adminCredential $directMicrosoftUpdates $csvFile
Version 2:
# updateWindowsList.ps1
# Version 0.0.2
# New feature over version 0.0.1: Simultaneous Executions
# Requirement: WinRM and Internet Access must be enabled on target computer(s)
#$computernames='SHERVER01','SHERVER02'
$directMicrosoftUpdates=$true
$csvFile='C:\updateResults.csv'
$adminCredential=$false # Leave this as false to obtain Domain Admin cred during runtime
function invokeWindowsUpdate{
[CmdletBinding()]
param (
[parameter(Mandatory=$true,Position=0)][string]$computer,
[parameter(Mandatory=$false,Position=1)][System.Management.Automation.PSCredential]$adminCredential,
[parameter(Mandatory=$false,Position=2)][bool]$microsoftUpdates=$false,
[parameter(Mandatory=$false,Position=3)][bool]$bypassWsus=$false,
[parameter(Mandatory=$false,Position=4)][int]$winRmPort=5985,
[parameter(Mandatory=$false,Position=5)][string]$logFile='C:\PSWindowsUpdate.log'
)
$ErrorActionPreference='stop'
<#
.SYNOPSIS
This script will automatically install all avaialable windows updates on a device and will automatically reboot if needed.
After reboots, windows updates will continue to run until all updates are installed.
# Features:
# - Check WSUS settings (bypass if required by the boolean value)
# - 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
# Future developments:
# - Detect and handle proxies
#>
function installPsWindowsUpdate{
$ErrorActionPreference='stop'
$psWindowsUpdateAvailable=Get-Module -ListAvailable -Name PSWindowsUpdate -EA SilentlyContinue;
if (!($psWindowsUpdateAvailable)){
try {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
if(Get-PackageProvider 'nuget' -ea SilentlyContinue -Force){
Install-PackageProvider -Name NuGet -Force
}
$null=Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted
$null=Install-Module PSWindowsUpdate -Confirm:$false -Force
$null=Import-Module PSWindowsUpdate -force
# Register Microsoft Update Service if it has not been registered
$microsoftUpdateId='7971f918-a847-4430-9279-4a52d1efe18d'
if (!($microsoftUpdateId -in (Get-WUServiceManager).ServiceID)){
Add-WUServiceManager -ServiceID $microsoftUpdateId -Confirm:$false
}
return $true;
}
catch{
write-host "Prerequisites not met on $ENV:COMPUTERNAME.";
return $false;
}
}else{
# Register Microsoft Update Service if it has not been registered
$microsoftUpdateId='7971f918-a847-4430-9279-4a52d1efe18d'
if (!($microsoftUpdateId -in (Get-WUServiceManager).ServiceID)){
Add-WUServiceManager -ServiceID $microsoftUpdateId -Confirm:$false
}
return $true
}
}
function checkPendingReboot([string]$computer=$ENV:computername,$session){
function checkRegistry{
if (Get-ChildItem "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending" -EA Ignore) { return $true }
if (Get-ChildItem "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootInProgress" -EA Ignore) { return $true }
if (Get-Item "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired" -EA Ignore) { return $true }
if (Get-Item "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\PackagesPending" -EA Ignore) { return $true }
if (Get-Item "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\PostRebootReporting" -EA Ignore) { return $true }
if (Get-Item 'HKLM:\SOFTWARE\Microsoft\ServerManager\CurrentRebootAttemps' -EA Ignore) { return $true }
if (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" -Name 'PendingFileRenameOperations' -EA Ignore) { return $true }
if (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" -Name 'PendingFileRenameOperations2' -EA Ignore) { return $true }
if (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce' -Name 'DVDRebootSignal' -EA Ignore) { return $true }
if (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon' -Name 'JoinDomain' -EA Ignore) { return $true }
if (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon' -Name 'AvoidSpnSet' -EA Ignore) { return $true }
try{ # This peruses CCM utility, if exists
$util = [wmiclass]"\\.\root\ccm\clientsdk:CCM_ClientUtilities"
$status = $util.DetermineIfRebootPending()
if(($null -ne $status) -and $status.RebootPending){
$result.SCCMRebootPending = $true
}
}catch{
return $false
}
}
if ($ENV:computername -eq $computer){
$result=checkRegistry;
}else{
$result=Invoke-Command -session $session -ScriptBlock{
param($importedFunc);
[ScriptBlock]::Create($importedFunc).Invoke();
} -ArgumentList ${function:checkRegistry}
}
return $result;
}
function clearRebootFlags($computer,$session,$verbose=$false){
if (checkPendingReboot $computer $session){
$isLocalHost=$env:computername -eq $computer
if($isLocalHost){
write-warning "$computer is LOCALHOST. Please reboot manually to clear reboot flags"
return $false
}else{
write-host "`nRestarting remote computer $computer to clear pending reboot flags..."
if($adminCredential){
Restart-Computer -Wait -ComputerName $computer -Credential $adminCredential -Force
}else{
Restart-Computer -Wait -ComputerName $computer -Force
}
write-host "$computer has been successfully restarted!" -ForegroundColor Yellow
return $true
}
}else{
if($verbose){write-host "There are no pending reboot flags to clear." -ForegroundColor Green}
return $true
}
}
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}
return $wuIsOn
}
function turnoffWsus{
# Turn WSUS settings OFF temporarily...
$wuPath="Registry::HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU";
$wuKey="UseWUServer";
Set-Itemproperty -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";
Set-Itemproperty -path $wuPath -Name $wuKey -value 1
restart-service wuauserv;
}
function connectWinRm($computer,$adminCredential,$winRmPort=5985){
if(!$computer){
write-warning "Computer name must be specified to initiate a WinRM connection."
return $false
}
# Legacy equivalent to Test-Netconnection
function checkNetConnection($computername,$port,$timeout=1000,$verbose=$false) {
$tcp = New-Object System.Net.Sockets.TcpClient;
try {
$connect=$tcp.BeginConnect($computername,$port,$null,$null)
$wait = $connect.AsyncWaitHandle.WaitOne($timeout,$false)
if(!$wait){
$null=$tcp.EndConnect($connect)
$tcp.Close()
if($verbose){
Write-Host "Connection Timeout" -ForegroundColor Red
}
Return $false
}else{
$error.Clear()
$null=$tcp.EndConnect($connect) # Dispose of the connection to release memory
if(!$?){
if($verbose){
write-host $error[0].Exception.Message -ForegroundColor Red
}
$tcp.Close()
return $false
}
$tcp.Close()
Return $true
}
} catch {
return $false
}
}
function enableWinRmRemotely($remoteComputer,$winRmPort,$adminCredential){
function Check-NetConnection($computername,$port,$timeout=1000,$verbose=$false) {
$tcp = New-Object System.Net.Sockets.TcpClient;
try {
$connect=$tcp.BeginConnect($computername,$port,$null,$null)
$wait = $connect.AsyncWaitHandle.WaitOne($timeout,$false)
if(!$wait){
$null=$tcp.EndConnect($connect)
$tcp.Close()
if($verbose){
Write-Host "Connection Timeout" -ForegroundColor Red
}
Return $false
}else{
$error.Clear()
$null=$tcp.EndConnect($connect) # Dispose of the connection to release memory
if(!$?){
if($verbose){
write-host $error[0].Exception.Message -ForegroundColor Red
}
$tcp.Close()
return $false
}
$tcp.Close()
Return $true
}
} catch {
return $false
}
}
if (!(get-command psexec)){
# Install Chocolatey
if (!(Get-Command choco.exe -ErrorAction SilentlyContinue)) {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12;
Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
}
choco install sysinternals -y;
}
$success=check-netconnection $remoteComputer $winRmPort
write-host 'Attempting to use psexec to enable WinRM remotely...'
if(!$adminCredential){ # Enable WinRM Remotely
$null=psexec.exe \\$remoteComputer -s C:\Windows\system32\winrm.cmd qc -quiet;
}else{
$username=$adminCredential.Username
$password=$adminCredential.GetNetworkCredential().Password
$null=psexec.exe \\$remoteComputer -u $username -p $password -s C:\Windows\system32\winrm.cmd qc -quiet
}
return check-netconnection $remoteComputer $winRmPort
}
# If machine is not pingable, wait five minutes
$fiveMinuteTimer=[System.Diagnostics.Stopwatch]::StartNew()
do{
$ping = Test-Connection $computer -quiet
if($ping -eq $false){sleep 1}
$pastFiveMinutes=$fiveMinuteTimer.Elapsed.TotalMinutes -ge 5
}until ($ping -eq $true -or $pastFiveMinutes)
$fiveMinuteTimer.stop()
$winRmAvailable=checkNetConnection $computer $winRmPort
if(!$winRmAvailable){
write-host "Attempting to enable WinRM on $computer" -ForegroundColor Yellow
$enableWinRmSuccessful=enableWinRmRemotely $computer
if($enableWinRmSuccessful){
write-host "WinRM enabled: $enableWinRmSuccessful"
}else{
write-warning "WinRM could not be enabled remotely. WinRM connection aborted."
return $false
}
}
# Wait for WinRm session prior to proceeding
if($session.state -eq 'Opened'){remove-pssession $session}
do{
$session=if($adminCredential){
try{
New-PSSession -ComputerName $computer -Credential $adminCredential -ea Stop
}catch{
New-PSSession -ComputerName $computer -Credential $adminCredential -SessionOption $(new-pssessionoption -IncludePortInSPN)
}
}else{
try{
New-PSSession -ComputerName $computer -ea Stop
}catch{
New-PSSession -ComputerName $computer -SessionOption $(new-pssessionoption -IncludePortInSPN)
}
}
sleep -seconds 1
if ($session){
write-host "Connected to $computer."
return $session
}
} until ($session.state -match "Opened")
}
function includePsWindowsUpdate($session){
$psWindowsUpdateIsAvailable=invoke-command -session $session -scriptblock {
param($installPsWindowsUpdate);
# Register Microsoft Update Service if it has not been registered
return [ScriptBlock]::Create($installPsWindowsUpdate).Invoke()
} -Args ${function:installPsWindowsUpdate}
if($psWindowsUpdateIsAvailable){
return $true
}else{
return $false
}
}
function cleanupWuJob($session,$logFile='C:\PSWindowsUpdate.log'){
invoke-command -Session $session -ScriptBlock {
param($logFile)
if (Get-ScheduledTask -TaskName "PSWindowsUpdate" -ErrorAction SilentlyContinue){
Write-Host "Removing PSWindowsUpdate scheduled task from $env:computername..."
Unregister-ScheduledTask -TaskName 'PSWindowsUpdate' -Confirm:$false};
if (Test-Path $logFile -ErrorAction SilentlyContinue){
Write-Host "Removing $logFile..."
Remove-item $logFile -force
}
} -Args $logFile
}
write-warning "$computer will go through Windows Update and will REBOOT AUTOMATICALLY (if necessary).`r`nPress Ctrl+C at anytime to cancel."
$session=connectWinRm $computer $adminCredential
if(!(includePsWindowsUpdate $session)){
remove-pssession $session
return $false
}
if($bypassWsus){
$wsusIsOn=Invoke-Command -session $session -scriptblock{
param($checkWsus);
[scriptblock]::create($checkWsus).invoke()
} -Args ${function:checkWsus}
if($wsusIsOn){
Invoke-Command -session $session -scriptblock{
param($turnoffWsus);
[scriptblock]::create($turnoffWsus).invoke()
} -args ${function:turnoffWsus}
}
}
Do{
#retrieves a list of available updates
write-host "Checking for new updates on $computer..."
$updates=invoke-command -session $session -scriptblock {
param($microsoftUpdates)
$executionPolicy=Get-ExecutionPolicy
if($executionPolicy -notmatch 'RemoteSigned|Unrestricted'){
Set-ExecutionPolicy RemoteSigned -force
}
$null=Import-Module PSWindowsUpdate
$availableUpdates=if($microsoftUpdates){
Get-wulist -MicrosoftUpdate -verbose
}else{
Get-Wulist -verbose
}
write-host $($availableUpdates|out-string).trim()
Set-ExecutionPolicy $executionPolicy -force
return $availableUpdates
} -Args $microsoftUpdates
$updatesCount=$updates.KB.count # $updates.count returns $null when count equals 1
# If there are available updates, proceed with installing the updates and then reboot the remote machine if required
if ($updatesCount){
#Invoke-WUJob will insert a scheduled task on the remote target as a mean to bypass 2nd hop issues
invoke-command -Session $session {
param($microsoftUpdates)
$logFile='C:\PSWindowsUpdate.log'
if(test-path $logFile){remove-item $logFile -force}
if($microsoftUpdates){
$invokeScript={
import-module PSWindowsUpdate;
Get-WindowsUpdate -AcceptAll -MicrosoftUpdate -Install | Out-File 'C:\PSWindowsUpdate.log'
}
}else{
$invokeScript={
import-module PSWindowsUpdate;
Get-WindowsUpdate -AcceptAll -Install | Out-File 'C:\PSWindowsUpdate.log'
}
}
Invoke-WUjob -ComputerName $env:computername -Script $invokeScript -Confirm:$false -RunNow
write-host "Windows Updates have been triggerred. Now checking for its log file...`r`n"
Do{
$logFileGenerated=if(test-path $logFile){get-content $logFile}else{$false}
if(!$logFileGenerated){
Start-Sleep -Seconds 1
write-host '.' -NoNewline
}
}until($logFileGenerated)
} -Args $microsoftUpdates
#Show update status until the amount of installed updates equals the same as the count of updates being reported
$dotLimit=10
$dotCount=0
$minute=0
$lastActivity=$null;
$installedCount=0;
Write-Host "`r`nThere $(if($updatesCount -eq 1){'is'}else{'are'}) $updatesCount pending update(s).";
do {
$getWindowsUpdateLog={param($logFile);Get-Content $logFile}
$updatestatus=Invoke-Command -Session $session -ScriptBlock $getWindowsUpdateLog -Args $logFile
$currentActivity=.{
$index=$updatestatus.count-1
do{
$value=$updatestatus[$index--]
}until($value)
return $value
}
$installedCount=([regex]::Matches($updatestatus, "Installed")).count
$updatesCompleted=$installedCount -eq $updatesCount
$failuresCount=([regex]::Matches($updatestatus, "Failed")).count
if ($currentActivity -ne $lastActivity){
if($currentActivity -match 'Installed'){
Write-Host "`r`nSuccessfully installed $installedCount of $updatesCount updates: $currentActivity" -ForegroundColor Green
}elseif($currentActivity -match 'Failed'){
Write-Host "`r`nFailed item: $currentActivity" -ForegroundColor Yellow
}else{
Write-Host "`r`nCurrent activity: $currentActivity"
}
$lastActivity=$currentActivity;
}else{
if ($dotCount++ -le $dotLimit){
Write-Host -NoNewline "."
}else{
$minute++
Write-Host "`r`nMinute count: $minute"
$dotCount=0
}
}
Start-Sleep -Seconds 6
}until ($updatesCompleted -or $failuresCount -ge 2)
}else{
write-host "There are $updatesCount pending updates detected."
}
if(checkPendingReboot $computer $session){
$localhostRebootFlag=!(clearRebootFlags $computer $session)
}else{
write-host "No pending reboots detected on $computer"
}
if($session.State -ne 'Opened'){
write-host "Reconnecting to $computer..." -ForegroundColor Yellow
$session=connectWinRm $computer $adminCredential
}
cleanupWuJob $session $logFile
if($updatesCompleted -or !$updatesCount){
Write-Host "`r`nWindows is now up to date on $computer" -ForegroundColor Green
}
}until(!$updatesCount -or $updatesCompleted -or $localhostRebootFlag)
if($bypassWsus -and $wsusIsOn){
write-host "Reverting WSUS registry edits"
Invoke-Command -session $session -scriptblock{
param($turnonWsus);
[scriptblock]::create($turnonWsus).invoke()
} -args ${function:turnonWsus} ;
}
if($session.State -eq 'Opened'){Remove-PSSession $session}
if($localhostRebootFlag){
write-host "$computer requires a reboot to complete the updates"
return $false
}else{
return $true
}
}
function confirmation($content,$testValue="I confirm",$maxAttempts=3){
$confirmed=$false;
$attempts=0;
$content|write-host
write-host "Please review this content for accuracy.`r`n"
while ($attempts -le $maxAttempts){
if($attempts++ -ge $maxAttempts){
write-host "A maximum number of attempts have reached. No confirmations received!`r`n"
break;
}
$userInput = Read-Host -Prompt "Please type in this value => $testValue <= to confirm. Input CANCEL to skip this item";
if ($userInput.ToLower() -eq $testValue.ToLower()){
$confirmed=$true;
write-host "Confirmed!`r`n";
break;
}elseif($userInput -like 'cancel'){
write-host 'Cancel command received.'
$confirmed=$false
break
}else{
Clear-Host;
$content|write-host
write-host "Attempt number $attempts of $maxAttempts`: $userInput does not match $testValue. Try again or Input CANCEL to skip this item`r`n"
}
}
return $confirmed;
}
function obtainDomainAdminCredentials{
# Legacy domain binding function
function isValidCred($u,$p){
# Get current domain using logged-on user's credentials
$domain = "LDAP://" + ([ADSI]"").distinguishedName
$domainCred = New-Object System.DirectoryServices.DirectoryEntry($domain,$u,$p)
if ($domainCred){
return $True
}else{
return $False
}
}
function isDomainAdmin($username){
if (!(get-module activedirectory)){Install-WindowsFeature RSAT-AD-PowerShell -Confirm:$false}
if((Get-ADUser $username -Properties MemberOf).MemberOf -match '^CN=Domain Admins'){
return $True;
}else{return $false;}
}
# Create a domain context
function dotNetDomainBind{
Add-Type -AssemblyName System.DirectoryServices.AccountManagement;
Try {
$type = [System.DirectoryServices.AccountManagement.ContextType]::Domain;
$context = New-Object System.DirectoryServices.AccountManagement.PrincipalContext $type,$env:USERDOMAIN;
return $context;
} Catch {
If ($_.Exception.InnerException -like "*The server could not be contacted*") {
write-host "Could not contact a server for the specified domain $env:USERDOMAIN via DotNet Method.";
} Else {
write-host "Unknown errors occured while attempting to contact $env:USERDOMAIN via DotNet Method.";
}
return $false;
}
}
$attempt=0;
$maxAttempts=3;
$plainTextPassword='';
Do{
$attempt++;
$failureMessage = $null;
[string][ValidateNotNullOrEmpty()]$userName=Read-Host -Prompt "Please input a User ID";
if($userName -notmatch '\\'){
$username=$env:USERDOMAIN+'\'+$userName
}
$password = Read-Host -Prompt "Please type in the password for user $userName" -AsSecureString;
$plainTextPassword=[Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($password))
# Test bind to this credential
try {
# Test bind
$context=dotNetDomainBind
if($context){
$validatedAccount = $context.ValidateCredentials($userName,$plainTextPassword)
}else{
$validatedAccount=isValidCred -u $userName -p $plainTextPassword
}
If ($validatedAccount) {
$onlyUserName=$userName -replace '.+\\'
$isDomainAdmin=isDomainAdmin -username $onlyUserName;
if($isDomainAdmin){
$credentials = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $userName,$password;
$validAccount=$true;
}else{
$failureMessage += "$attempt out of $maxAttempts`: $userName account is valid, but it is not a Domain Admin";
}
}else{
$failureMessage += "$attempt out of $maxAttempts`: username and/or password error";
}
}catch{
write-warning $_
$failureMessage += "Unable to bind to $env:USERDNSDOMAIN.";
}
# Depending on whether there are failures, proceed accordingly
if($failureMessage){
If ($attempt -lt $maxAttempts-1) {
$message = "$failureMessage`: Authentication error. Please Try again.";
Write-Warning $message;
}elseif($attempt -eq $maxAttempts-1){
$message = "$failureMessage`: Last attempt.";
Write-Warning $message;
$credentials= $false;
}
}
} Until (($ValidAccount) -or ($Attempt -ge $MaxAttempts))
if($credentials){return $credentials;}else{return $false}
}
function updateWindows($computerNames,$adminCredential,$microsoftUpdates=$false,$csvFile='C:\updateResults.csv'){
get-job|stop-job|remove-job -force
foreach ($computerName in $computerNames){
Start-Job -name $computerName -ScriptBlock {
param($invokeWindowsUpdate,$computerName,$adminCredential,$microsoftUpdates)
[scriptblock]::create($invokeWindowsUpdate).invoke($computerName,$adminCredential,$microsoftUpdates)
} -Args ${function:invokeWindowsUpdate},$computerName,$adminCredential,$microsoftUpdates
}
$lineBreak=30
$dotCount=0
$minute=0
write-host "Minute`r`n$minute`:" -NoNewline -ForegroundColor Yellow
$jobResults=@()
$jobsCount=(get-job).count
do{
$completedJobs=get-job|?{$_.State -eq 'Completed'}
foreach ($job in $completedJobs){
$computer=$job.Name
write-host "`r`n===================================================`r`n$computer job completed with these messages:`r`n===================================================`r`n"
$timeStamp=$job.PSBeginTime
$minutesElapsed=[math]::round(($job.PSEndTime-$job.PSBeginTime).TotalMinutes,2)
$computer=$job.Name
$completed=receive-job -id $job.id
$result=[pscustomobject][ordered]@{
computerName=$computer
timeStamp=$timeStamp
minutesElapsed=$minutesElapsed
completed=$completed
}
$jobResults+=,$result
remove-job -id $job.id
}
if($dotCount++ -lt $lineBreak){
write-host '.' -NoNewline
}else{
$minute++
write-host "`r`n$minute`t:" -ForegroundColor Yellow -NoNewline
$dotCount=0
}
Start-Sleep -seconds 2
}until($jobsCount -eq $jobResults.count -or !(get-job))
write-host "`r`n$(($jobResults|ft|out-string).trim())"
$jobResults | Export-Csv -Path $csvFile -NoTypeInformation -Append
write-host "Windows Update results have been exported to: $csvFile"
$localMachineIsPendingReboot=($jobResults|?{$_.computername -eq $env:computername}).pendingReboot -eq $true
if($localMachineIsPendingReboot){
$confirmed=confirmation "I want to reboot this local machine $env:computername"
if($confirmed){
Restart-Computer $env:computerName -force
}else{
write-warning "Please reboot $env:computername manually to complete its updates."
}
}else{
write-host 'Done.' -ForegroundColor Green
}
}
if(!$adminCredential){$adminCredential=obtainDomainAdminCredentials}
updateWindows $computerNames $adminCredential $directMicrosoftUpdates $csvFile
# updateMultipleWindows.ps1
# Version 0.0.1
# Requirement: WinRM and Internet Access must be enabled on target computer(s)
$computernames='LAX-SERVER01','LAX-SERVER02'
$csvFile='C:\updateResults.csv'
$adminCredential=$false # Leave this as false to obtain Domain Admin cred during runtime
function updateRemoteWindows{
[CmdletBinding()]
param (
[parameter(Mandatory=$true,Position=0)][string]$computer,
[parameter(Mandatory=$false,Position=1)][System.Management.Automation.PSCredential]$adminCredential,
[parameter(Mandatory=$false,Position=2)][bool]$microsoftUpdates=$true,
[parameter(Mandatory=$false,Position=3)][bool]$bypassWsus=$false,
[parameter(Mandatory=$false,Position=4)][int]$winRmPort=5985,
[parameter(Mandatory=$false,Position=5)][string]$logFile='C:\PSWindowsUpdate.log'
)
$ErrorActionPreference='stop'
<#
.SYNOPSIS
This script will automatically install all avaialable windows updates on a device and will automatically reboot if needed.
After reboots, windows updates will continue to run until all updates are installed.
# Features:
# - Check WSUS settings (bypass if required by the boolean value)
# - 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
# Future developments:
# - Detect and handle proxies
#>
function installPsWindowsUpdate{
$ErrorActionPreference='stop'
$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;
return $true;
}
catch{
"Prerequisites not met on $ENV:COMPUTERNAME.";
return $false;
}
}else{
return $true
}
}
function checkPendingReboot([string]$computer=$ENV:computername,$session){
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(($null -eq $status) -and $status.RebootPending){
return $true
}else{
return $false
}
}catch{
write-warning $_
return $false
}
}
if ($ENV:computername -eq $computer){
$result=checkRegistry;
}else{
$result=Invoke-Command -session $session -ScriptBlock{
param($importedFunc);
[ScriptBlock]::Create($importedFunc).Invoke();
} -ArgumentList ${function:checkRegistry}
}
return $result;
}
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}
return $wuIsOn
}
function turnoffWsus{
# Turn WSUS settings OFF temporarily...
$wuPath="Registry::HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU";
$wuKey="UseWUServer";
Set-Itemproperty -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";
Set-Itemproperty -path $wuPath -Name $wuKey -value 1
restart-service wuauserv;
}
function connectWinRm($computer,$adminCredential,$winRmPort=5985){
# Legacy equivalent to Test-Netconnection
function checkNetConnection($computername,$port,$timeout=1000,$verbose=$false) {
$tcp = New-Object System.Net.Sockets.TcpClient;
try {
$connect=$tcp.BeginConnect($computername,$port,$null,$null)
$wait = $connect.AsyncWaitHandle.WaitOne($timeout,$false)
if(!$wait){
$null=$tcp.EndConnect($connect)
$tcp.Close()
if($verbose){
Write-Host "Connection Timeout" -ForegroundColor Red
}
Return $false
}else{
$error.Clear()
$null=$tcp.EndConnect($connect) # Dispose of the connection to release memory
if(!$?){
if($verbose){
write-host $error[0].Exception.Message -ForegroundColor Red
}
$tcp.Close()
return $false
}
$tcp.Close()
Return $true
}
} catch {
return $false
}
}
function enableWinRmRemotely($remoteComputer,$winRmPort,$adminCredential){
function Check-NetConnection($computername,$port,$timeout=1000,$verbose=$false) {
$tcp = New-Object System.Net.Sockets.TcpClient;
try {
$connect=$tcp.BeginConnect($computername,$port,$null,$null)
$wait = $connect.AsyncWaitHandle.WaitOne($timeout,$false)
if(!$wait){
$null=$tcp.EndConnect($connect)
$tcp.Close()
if($verbose){
Write-Host "Connection Timeout" -ForegroundColor Red
}
Return $false
}else{
$error.Clear()
$null=$tcp.EndConnect($connect) # Dispose of the connection to release memory
if(!$?){
if($verbose){
write-host $error[0].Exception.Message -ForegroundColor Red
}
$tcp.Close()
return $false
}
$tcp.Close()
Return $true
}
} catch {
return $false
}
}
if (!(get-command psexec)){
# Install Chocolatey
if (!(Get-Command choco.exe -ErrorAction SilentlyContinue)) {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12;
Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
}
choco install sysinternals -y;
}
$success=check-netconnection $remoteComputer $winRmPort
write-host 'Attempting to use psexec to enable WinRM remotely...'
if(!$adminCredential){ # Enable WinRM Remotely
$null=psexec.exe \\$remoteComputer -s C:\Windows\system32\winrm.cmd qc -quiet;
}else{
$username=$adminCredential.Username
$password=$adminCredential.GetNetworkCredential().Password
$null=psexec.exe \\$remoteComputer -u $username -p $password -s C:\Windows\system32\winrm.cmd qc -quiet
}
return check-netconnection $remoteComputer $winRmPort
}
do{
$ping = Test-Connection $computer -quiet
if($ping -eq $false){sleep 1}
}until ($ping -eq $true)
$winRmAvailable=checkNetConnection $computer $winRmPort
if(!$winRmAvailable){
write-host "Attempting to enable WinRM on $computer" -ForegroundColor Yellow
$enableWinRmSuccessful=enableWinRmRemotely $computer
if($enableWinRmSuccessful){
write-host "WinRM enabled: $enableWinRmSuccessful"
}else{
write-warning "WinRM could not be enabled remotely. WinRM connection aborted."
return $false
}
}
# Wait for WinRm session prior to proceeding
do{
if($adminCredential){
$session = New-PSSession -ComputerName $computer -Credential $adminCredential
}else{
$session = New-PSSession -ComputerName $computer
}
write-host "Connecting to remote computer $computer..."
sleep -seconds 1
if ($session){
write-host "Connected."
return $session
}
} until ($session.state -match "Opened")
}
# Ensure that this function does not execute on the localhost
if($env:computername,'localhost'|?{$_ -like "$computer*"}){
write-warning "$computer is detected as the localhost where this program is invoked. This is out of scope of this function."
return $false
}
# Advisories
write-warning "$computer will go through Windows Update and will REBOOT AUTOMATICALLY (if necessary). Press Ctrl+C at anytime to cancel."
$session=connectWinRm $computer $adminCredential
Do{
# Install prerequisites
$psWindowsUpdateAvailable=invoke-command -session $session -scriptblock {
param($installPsWindowsUpdate);
[ScriptBlock]::Create($installPsWindowsUpdate).Invoke();
} -Args ${function:installPsWindowsUpdate}
if(!$psWindowsUpdateAvailable){
write-warning "$computername`t: PSWindowsUpdate installation failed."
return $false
}
#retrieves a list of available updates
write-host "Checking for new updates on $computer..."
$updates=invoke-command -session $session -scriptblock {
param($microsoftUpdates)
if($(Get-ExecutionPolicy) -ne 'RemoteSigned'){
Set-ExecutionPolicy RemoteSigned -force
}
$null=Import-Module PSWindowsUpdate
if($microsoftUpdates){
Get-wulist -MicrosoftUpdate -verbose
}else{
Get-wulist -WindowsUpdate -verbose
}
} -Args $microsoftUpdates
# Count how many updates are available
$updatesCount = ($updates.kb).count
# If there are available updates proceed with installing the updates and then reboot the remote machine if required
if ($null -ne $updates){
if($bypassWsus){
$wsusIsOn=Invoke-Command -session $session -scriptblock{
param($checkWsus);
[scriptblock]::create($checkWsus).invoke()
} -Args ${function:checkWsus}
if($wsusIsOn){
Invoke-Command -session $session -scriptblock{
param($turnoffWsus);
[scriptblock]::create($turnoffWsus).invoke()
} -args ${function:turnoffWsus}
}
}
#Invoke-WUJob will insert a scheduled task on the remote target as a mean to bypass 2nd hop issues
invoke-command -Session $session {
$invokeScript={
param($microsoftUpdates)
import-module PSWindowsUpdate;
if($microsoftUpdates){
# Register Microsoft Update Service if it has not been registered
$microsoftUpdateId='7971f918-a847-4430-9279-4a52d1efe18d'
if (!($microsoftUpdateId -in (Get-WUServiceManager).ServiceID)){
Add-WUServiceManager -ServiceID $microsoftUpdateId -Confirm:$false
}
Get-WindowsUpdate -AcceptAll -MicrosoftUpdate -Install | Out-File C:\PSWindowsUpdate.log
}else{
Get-WindowsUpdate -AcceptAll -WindowsUpdate -Install | Out-File C:\PSWindowsUpdate.log
}
}
Invoke-WUjob -ComputerName $env:computername -Script $invokeScript -Confirm:$false -RunNow
} -Args $microsoftUpdates
#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
$dotCount=0
$lastActivity="";
$installedCount=0;
Write-Host "There $(if($updatesCount -eq 1){'is'}else{'are'}) $updatesCount pending update(s)`r`n";
do {
$getWindowsUpdateLog={Get-Content "C:\PSWindowsUpdate.log"}
$updatestatus=Invoke-Command -Session $session -ScriptBlock $getWindowsUpdateLog
$currentActivity=$updatestatus | select-object -last 1
if (($currentActivity -ne $lastActivity) -AND ($Null -ne $currentActivity)){
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
$installedCount = ([regex]::Matches($updatestatus, "Installed")).count
}until ($installedCount -eq $updatesCount)
#restarts the remote computer and waits till it starts up again
if (checkPendingReboot $computer $session){
write-host "`nReboots required.`nRestarting remote computer $computer to clear pending reboot flags."
if($adminCredential){
Restart-Computer -Wait -ComputerName $computer -Credential $adminCredential -Force
}else{
Restart-Computer -Wait -ComputerName $computer -Force
}
write-host "$computer has been successfully restarted!" -ForegroundColor Yellow
if($session.State -ne 'Opened'){
write-host "Reconnecting to $computer..." -ForegroundColor Yellow
$session=connectWinRm $computer $adminCredential
}
}else{
write-host "No reboots required." -ForegroundColor Green
}
}else{
write-host "There are no available patches detected on $computer"
}
}until(($null -eq $updates) -OR ($installedCount -eq $updatesCount))
if($session.State -ne 'Opened'){
write-host "Reconnecting to $computer..." -ForegroundColor Yellow
$session=connectWinRm $computer $adminCredential
}
invoke-command -Session $session -ScriptBlock {
param($logFile)
if (Get-ScheduledTask -TaskName "PSWindowsUpdate" -ErrorAction SilentlyContinue){
Write-Host "Removing PSWindowsUpdate scheduled task from $env:computername..."
Unregister-ScheduledTask -TaskName PSWindowsUpdate -Confirm:$false};
if (Test-Path $logFile -Credential $adminCredential -ErrorAction SilentlyContinue){
Write-Host "Deleting log to prevent collisions with subsequent runs."
Write-Host "Removing $logFile."
Remove-item $logFile
}
} -Args $logFile
if($bypassWsus -and $wsusIsOn){
write-host "Reverting WSUS registry edits"
Invoke-Command -session $session -scriptblock{
param($turnonWsus);
[scriptblock]::create($turnonWsus).invoke()
} -args ${function:turnonWsus} ;
}
Write-Host "Windows is now up to date on $computer" -ForegroundColor Green
if($session.State -eq 'Opened'){Remove-PSSession $session}
return $true
}
function updateLocalWindows{
param(
$microsoftUpdates=$true,
$bypassWsus=$false,
$notCategory='Drivers',
$verbose=$false
)
if($verbose){$localTimer=[System.Diagnostics.Stopwatch]::StartNew()}
$tempDir='C:\temp\'
if(Test-Path $tempDir){$null=mkdir $tempDir -force}
function checkPendingReboot{
function checkRegistryPendingReboot{
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
}
return checkRegistryPendingReboot
}
function updateWindows($microsoftUpdates,$notCategory){
#Install the PowerShell Windows Update module
$checkModule=Get-Module -ListAvailable -Name PSWindowsUpdate
if(!($checkModule)){
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
# Set PowerShell Gallery as Trusted to bypass prompts
#$trustPSGallery=(Get-psrepository -Name 'PSGallery').InstallationPolicy
If($trustPSGallery -ne 'Trusted'){
Install-PackageProvider -Name Nuget -Force
#Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted
}
Install-Module PSWindowsUpdate -Confirm:$false
}
# Perform Updates
set-executionpolicy bypass -force
if($bypassWsus){
}
# -MicrosoftUpdate: include other Microsoft products (Office, Silverlight, Visual C++, etc.)
# -WindowsUpdate: include only Windows updates
if($microsoftUpdates){
# Register Microsoft Update Service if it has not been registered
$microsoftUpdateId='7971f918-a847-4430-9279-4a52d1efe18d'
if (!($microsoftUpdateId -in (Get-WUServiceManager).ServiceID)){
Add-WUServiceManager -ServiceID $microsoftUpdateId -Confirm:$false
}
$updateCommand='Get-WindowsUpdate -AcceptAll -MicrosoftUpdate -Install -IgnoreReboot'+$(if($notCategory){' -NotCategory '+$notCategory})
}else{
$updateCommand='Get-WindowsUpdate -AcceptAll -WindowsUpdate -Install -IgnoreReboot'+$(if($notCategory){' -NotCategory '+$notCategory})
}
Invoke-Expression $updateCommand
$pendingReboot=checkPendingReboot $computer
if(!$pendingReboot){
write-host 'Updates are completed.' -ForegroundColor Green
return $true
}else{
write-host 'Please reboot and trigger updates again to complete the process.' -ForegroundColor Yellow
return $false
}
}
function checkUpdates($microsoftUpdates,$notCategory='drivers',$verbose=$false){
## This runs into the constraints of unelevated process being prevented from reading stdout of an elevated session
## Source: https:// docs.microsoft.com/en-us/dotnet/api/system.diagnostics.process?view=netcore-3.1
#$startInfo = New-Object System.Diagnostics.ProcessStartInfo
#$startInfo.FileName = $exe
#$startInfo.RedirectStandardError = $true
#$startInfo.RedirectStandardOutput = $true
#$startInfo.Arguments = "(Get-wulist -verbose).kb.count"
#$startInfo.CreateNoWindow = $false
#$startInfo.UseShellExecute = $false
##$startInfo.Verb = "runas"
##$startInfo.UseShellExecute = $true
##Error when trying to run as Administrator: Exception calling 'Start' with "0" argument(s):
## 'The Process object must have the UseShellExecute property set to false in order to redirect IO streams.'
#$process = New-Object System.Diagnostics.Process
#$process.StartInfo = $startInfo
#$process.Start()
#$process.WaitForExit()
#$stdout = $process.StandardOutput.ReadToEnd()
#$stderr = $process.StandardError.ReadToEnd()
#Write-Host 'stdout: '+ $stdout
#if($stderr){Write-Host 'stderr: '+$stderr -ForegroundColor Red}
#Write-Host 'exit code: '+$($process.ExitCode)
#return $stdout
# This is the workaround
$tempFile='C:\temp\availableUpdatesCount.txt'
$checkUpdates=@"
`$x=(Get-wulist -verbose $(if($microsoftUpdates){'-MicrosoftUpdate '}) $(if($notCategory){'-NotCategory '+$notCategory})).kb.count
write-host `$x
New-Item '$tempFile' -ItemType File -Value `$x -Force
"@
Start-Process -verb Runas powershell.exe $checkUpdates -Wait -WindowStyle $(if($verbose){'normal'}else{'minimized'})
$result=Get-Content $tempFile
Start-Process -verb Runas powershell.exe "remove-item '$tempFile' -Force" -WindowStyle hidden -Wait
return $result
}
$functionScript = @"
`$Host.UI.RawUI.BackgroundColor = 'Black'
`$myWindowsID=[System.Security.Principal.WindowsIdentity]::GetCurrent()
`$myWindowsPrincipal=new-object System.Security.Principal.WindowsPrincipal(`$myWindowsID)
`$adminRole=[System.Security.Principal.WindowsBuiltInRole]::Administrator
if (`$myWindowsPrincipal.IsInRole(`$adminRole)){
write-host "This session is running under the context of the system 'Administrator'" -ForegroundColor Green
}else{
write-warning "This session is NOT in the context of 'Administrator'"
}
Function checkPendingReboot{
$(Get-Command checkPendingReboot|Select -expand Definition)
}
Function updateWindows{
$(Get-Command updateWindows|Select -expand Definition)
}
updateWindows $microsoftUpdates $notCategory
"@
write-host "Spawning a background process to update Windows as Administrator..."
do{
$process=Start-Process -verb Runas powershell $functionScript -WindowStyle $(if($verbose){'normal'}else{'minimized'}) -PassThru
$lineBreak=60
$dotCount=0
$minute=0
write-host "Minute`r`n$minute`t:" -NoNewline -ForegroundColor Yellow
do {
if($dotCount++ -lt $lineBreak){
write-host '.' -NoNewline
}else{
$minute++
write-host "`r`n$minute`t:" -ForegroundColor Yellow -NoNewline
$dotCount=0
}
Start-Sleep -s 1
}until (!$process.Responding)
[bool]$isPendingReboot=checkPendingReboot
[int]$updatesAvailable=checkUpdates $microsoftUpdates $notCategory $verbose
if(!$updatesAvailable){
write-host "`r`nWindows updates completed" -ForegroundColor Green
$completed=$true
}else{
write-host "`r`nMissing $updatesAvailable updates." -ForegroundColor Yellow
$completed=$false
}
}until($completed -or $isPendingReboot)
if($verbose){
$minutes=[math]::round($localTimer.Elapsed.TotalMinutes,2)
write-host "$minutes minutes elapsed"
}
if($isPendingReboot){
write-host "`r`nPending reboots detected." -ForegroundColor Yellow
return $false
}elseif($completed){
return $true
}
}
function confirmation($content,$testValue="I confirm",$maxAttempts=3){
$confirmed=$false;
$attempts=0;
$content|write-host
write-host "Please review this content for accuracy.`r`n"
while ($attempts -le $maxAttempts){
if($attempts++ -ge $maxAttempts){
write-host "A maximum number of attempts have reached. No confirmations received!`r`n"
break;
}
$userInput = Read-Host -Prompt "Please type in this value => $testValue <= to confirm. Input CANCEL to skip this item";
if ($userInput.ToLower() -eq $testValue.ToLower()){
$confirmed=$true;
write-host "Confirmed!`r`n";
break;
}elseif($userInput -like 'cancel'){
write-host 'Cancel command received.'
$confirmed=$false
break
}else{
cls;
$content|write-host
write-host "Attempt number $attempts of $maxAttempts`: $userInput does not match $testValue. Try again or Input CANCEL to skip this item`r`n"
}
}
return $confirmed;
}
# Obtain Domain Admin Credentials
function obtainDomainAdminCredentials{
# Legacy domain binding function
function isValidCred($u,$p){
# Get current domain using logged-on user's credentials
$domain = "LDAP://" + ([ADSI]"").distinguishedName
$domainCred = New-Object System.DirectoryServices.DirectoryEntry($domain,$u,$p)
if ($domainCred){
return $True
}else{
return $False
}
}
function isDomainAdmin($username){
if (!(get-module activedirectory)){Install-WindowsFeature RSAT-AD-PowerShell -Confirm:$false}
if((Get-ADUser $username -Properties MemberOf).MemberOf -match '^CN=Domain Admins'){
return $True;
}else{return $false;}
}
# Create a domain context
function dotNetDomainBind{
Add-Type -AssemblyName System.DirectoryServices.AccountManagement;
Try {
$type = [System.DirectoryServices.AccountManagement.ContextType]::Domain;
$context = New-Object System.DirectoryServices.AccountManagement.PrincipalContext $type,$env:USERDOMAIN;
return $context;
} Catch {
If ($_.Exception.InnerException -like "*The server could not be contacted*") {
write-host "Could not contact a server for the specified domain $env:USERDOMAIN via DotNet Method.";
} Else {
write-host "Unknown errors occured while attempting to contact $env:USERDOMAIN via DotNet Method.";
}
return $false;
}
}
$attempt=0;
$maxAttempts=3;
$plainTextPassword='';
Do{
$attempt++;
$failureMessage = $null;
[string][ValidateNotNullOrEmpty()]$userName=Read-Host -Prompt "Please input a User ID";
if($userName -notmatch '\\'){
$username=$env:USERDOMAIN+'\'+$userName
}
$password = Read-Host -Prompt "Please type in the password for user $userName" -AsSecureString;
$plainTextPassword=[Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($password))
# Test bind to this credential
try {
# Test bind
$context=dotNetDomainBind
if($context){
$validatedAccount = $context.ValidateCredentials($userName,$plainTextPassword)
}else{
$validatedAccount=isValidCred -u $userName -p $plainTextPassword
}
If ($validatedAccount) {
$onlyUserName=$userName -replace '.+\\'
$isDomainAdmin=isDomainAdmin -username $onlyUserName;
if($isDomainAdmin){
$credentials = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $userName,$password;
$validAccount=$true;
}else{
$failureMessage += "$attempt out of $maxAttempts`: $userName account is valid, but it is not a Domain Admin";
}
}else{
$failureMessage += "$attempt out of $maxAttempts`: username and/or password error";
}
}catch{
write-warning $_
$failureMessage += "Unable to bind to $env:USERDNSDOMAIN.";
}
# Depending on whether there are failures, proceed accordingly
if($failureMessage){
If ($attempt -lt $maxAttempts-1) {
$message = "$failureMessage`: Authentication error. Please Try again.";
Write-Warning $message;
}elseif($attempt -eq $maxAttempts-1){
$message = "$failureMessage`: Last attempt.";
Write-Warning $message;
$credentials= $false;
}
}
} Until (($ValidAccount) -or ($Attempt -ge $MaxAttempts))
if($credentials){return $credentials;}else{return $false}
}
function outputHashtableToCsv{
param(
$hashTable,
$csvFile='C:\updateResults.csv',
$headers=@('computerName','minutesToUpdate')
)
try{
write-host $($hashTable|out-string)
if(test-path $csvFile){
rename-item $csvFile "$csvFile.bak"
write-warning "$csvFile currently exists. Hence, that previous file has been renamed to $csvFile.bak"
}
$hashTable.GetEnumerator() | `
Select-Object -Property @{N=$headers[0];E={$_.Key}}, @{N=$headers[1];E={$_.Value}} | `
Export-Csv -NoTypeInformation -Path $csvFile
write-host "CSV has been created successfully: $csvFile"
return $true
}catch{
write-warning $_
return $false
}
}
function updateWindows($computerNames,$adminCredential,$csvFile='C:\updateResults.csv'){
#get-job|?{$_.HasMoreData -eq $false -and $_.State -match 'Completed|failed'}|remove-job
get-job|remove-job
foreach ($computerName in $computerNames){
if($env:computername -eq $computerName){
#$timer=[System.Diagnostics.Stopwatch]::StartNew()
#write-host "Updating localhost $computername..."
Start-Job -name $computerName -ScriptBlock {
param($updateLocalWindows)
[scriptblock]::create($updateLocalWindows).invoke()
} -Args ${function:updateLocalWindows}
#$localMachineIsPendingReboot=!(updateLocalWindows)
}else{
Start-Job -name $computerName -ScriptBlock {
param($updateRemoteWindows,$computerName,$adminCredential)
[scriptblock]::create($updateRemoteWindows).invoke($computerName,$adminCredential)
} -Args ${function:updateRemoteWindows},$computerName,$adminCredential
#updateRemoteWindows -computer $computerName -adminCredential $adminCredential
}
#$minutesElapsed=[math]::round($timer.Elapsed.TotalMinutes,2)
#write-host "$computerName update minutes: $minutesElapsed"
#$updateResults+=@{$computerName=$minutesElapsed}
}
$lineBreak=60
$dotCount=0
$minute=0
write-host "Minute`r`n$minute`t:" -NoNewline -ForegroundColor Yellow
$GLOBAL:jobResults=@()
do{
$completedJobs=get-job|?{$_.HasMoreData -eq $true -and $_.State -eq 'Completed'}
foreach ($job in $completedJobs){
$minutesElapsed=[math]::round(($job.PSEndTime-$job.PSBeginTime).TotalMinutes,2)
$computer=$job.Name
write-host "Processing $computer results..."
$result=receive-job -id $job.id
$jobResults+=[pscustomobject]@{
computerName=$computer
minutesElapsed=$minutesElapsed
completed=$result
}
remove-job -id $job.id
}
if($dotCount++ -lt $lineBreak){
write-host '.' -NoNewline
}else{
$minute++
write-host "`r`n$minute`t:" -ForegroundColor Yellow -NoNewline
$dotCount=0
}
Start-Sleep -s 1
}until(!(get-job))
write-host "$(($jobResults|ft|out-string).trim())"
$localMachineIsPendingReboot=($jobResults|?{$_.computername -eq $env:computername}).completed -eq $false
if($localMachineIsPendingReboot){
$confirmed=confirmation "I want to reboot this local machine $env:computername"
if($confirmed){
Restart-Computer $env:computerName -force
}else{
write-warning "Please reboot $env:computername manually to complete its updates."
}
}else{
write-host 'Done.' -ForegroundColor Green
}
}
if(!$adminCredential){$adminCredential=obtainDomainAdminCredentials}
updateWindows $computerNames $adminCredential $csvFile
Categories:
Marc Hagemann
Hi,
I’ve just tried out the script above to update severeal servers remotely. The servers have noc connection to the Internet. It’ seems like the scrit works. But if I run it a second time, it will end in :
…………….
1 :…………………………
2 :…………………………
3 :…………………………
4 :…………………………
5 :…………………………
6 :…………………………
7 :…………………………
8 :…………………………
9 :…………………………
10 :…………………………
11 :…………………………
12 :…………………………
13 :…………………………
14 :…………………………
15 :…………………………
16 :…………………
and ongoing. I have to abort it with ctrl+c.
There is also a message for each server:
==================================================
job completed with these messages:
===================================================
WARNING: will go through Windows Update and will REBOOT AUTOMATICALLY (if necessary).
Press Ctrl+C at anytime to cancel.
Connected to .
Checking for new updates on …
VERBOSE: (08.12.2020 14:10:10): Connecting to Microsoft Update server. Please wait…
Exception calling “Invoke” with “3” argument(s): “The running command stopped because the preference variable “ErrorActionPreference” or common parameter is set to Stop: An operation did not
complete because the service is not in the data store. Probably you don’t have permission for remote connection to Windows Update Agent or used wrong ServiceID.”
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : ActionPreferenceStopException
+ PSComputerName : localhost
The servers have already downloaded updates, which may have to be installed or a server restard is outstanding. can someone shorten the script for me, so that it only check if local downloaded updates, or wsus updates are available and restart the server?
Regards
Marc
kimconnect
Hi Marc,
Thanks for trying this out. Script does require that the target Windows have a connection to the Internet or WSUS. Let me find some time to modify this to account for pre-downloaded packages, where no Internet connections would be required. Meanwhile, can you give me more details about your remote computers: are they downloading packages from your local WSUS?
Best regards,
Kim
Marc Hagemann
Thank you,Kim, for your big efforts.
We supply various customers with our own software and manage the servers on which our software is installed. However, many companies give Windows their own parameters through group policies. So does the current customer for whom I need the Powerschell script. With this customer, the updates are automatically downloaded to the server and you are asked to install them manually and to restart the server. So it’s really only a matter of displaying these already downloaded updates on the server, installing them and restarting the server (all remotely from a single server running this script).Since there are about 30 servers that we have to provide with Windows updates ourselves, such a script would be a great relief.
I’ve tried a script like this before, but it never worked and didn’t work remotely. Yours, however, seems to be going in the right direction for my needs.
I would like to have an output in the script that I can send by email when the updates have gone through successfully. I would like to have information on which server, which update was installed and when it was then re-evaluated.
I can add the part by e-mail myself. I’ve already finished it. I would only need the listing as output.
kimconnect
Marc,
I’ve been preoccupied with things lately. I’ll take a closer look a the error message you’re showing and revise the script to account for that scenario. It does seem that your target machines do not have access to the Internet to contact Microsoft; hence, ‘service is not in the data store.’
Will update this post when I make progress.
Cheers,
Marc Hagemann
Thank you! Yes, servers have no comnnection to the internet and updates are predowanloaded by customers group policies settings, I cannot change. The customer only want us to check if updates for installation are available on the servers, install them and restart the servers, if necessary.
Your main script is also very useful for me for other customers too.
kimconnect
Marc,
Give version 0.0.3 a try and let me know if it enables you to update servers that have no Internet connectivity.
Best regards,
KimConnect
Marc Hagemann
Thank you. I just tried out. I’m not that Powershell pro. So I entered Admin credentzials on the section on the top of the script. When I run the script, It asks for another credentials:
“Please input a User ID: ”
If I type in nothing, it says
“Could not contact a server for the specified domain via DotNet Method.
WARNING: Cannot validate argument on parameter ‘Identity’. The Identity property on the argument is null or empty.
WARNING: Unable to bind to .: Authentication error. Please Try again.”
If I type in admin credentials with domain, it says:
“Could not contact a server for the specified domain via DotNet Method.
WARNING: The server has rejected the client credentials.
WARNING: Unable to bind to .: Last attempt.”
Why I have to enter these User ID? For what case is it? Why not use the pre entered Admin credentials on the top? Can I bypass this? The scipt should run a scheduled Task, so that I cannot type in credentials manually, whe it runs
kimconnect
I’ve been working on too many tasks at the same time, so I’ve made mistakes in updating the program. Please retry ‘version 3’ to see if it now works for ya.
Marc Hagemann
No problem. This is a long scipt.
The updated version seems to run more smooth. The only thing , it stucks (even in the past versions ) is on this point:
— —- ————- —– ———– ——– ——-
59 testWindows BackgroundJob Running True localhost …
Minute
0:………
===================================================
testWindows job completed with these messages:
===================================================
WARNING: testWindows will go through Windows Update and will REBOOT AUTOMATICALLY (if necessary).
Press Ctrl+C at anytime to cancel.
Connected to testWindows.
testWindows: no internet connectivity detected
Adding task on testWindows…
-Message windowsUpdate has taken 0.5019489 seconds to initiate
Checking \\testWindows\c$\WindowsUpdate.log for Windows Update statuses…
Exception calling “Invoke” with “3” argument(s): “The running command stopped because the preference variable “ErrorActionPreference” or common parameter is set to Stop: Access is denied”
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : ActionPreferenceStopException
+ PSComputerName : localhost
…………………
1 :…………………………
2 :…………………………
3 :…………………………
4 :…………………………
5 :…………………………
6 :
I only tested it with one server on the same time , but the warning:
Exception calling “Invoke” with “3” argument(s): “The running command stopped because the preference variable “ErrorActionPreference” or common parameter is set to Stop: Access is denied”
Appears on every server. If I comment it out. It runs without this message but in an andles retry where the dots are counting
Marc Hagemann
I just looked in the logfile of the servers, I’ll try to update. The write “2 updates detected” which is correct, but after that, the script seems to wait for something. no installation process, no reboot of the server.
kimconnect
The server(s) was waiting for a reboot, which the script would perform. Afterward, updates would continue until done. I’ve misspelled the $adminCredential as $credential. That’s been fixed. Please give it another try.
Marc Hagemann
So, I tried again.
An Additional Info: I have noc domain admin account. I only have an account with local admin rights on the target computers.
->I run the script
-> The script finds the Update (can see it in the logfile on each server)
-> Th log says, that server needs to be reboot.
-> I see an scheduled task, generated
-> lokked into the powershell script, which is opend due to the task
->I run it manually and it says ” has 0 available updates. Patching FINISHED”
-> This is written on every server. If I look in the Windowes Updates, there are updates pending and renboot required
-> On this part, there must be a little mistake in the script, I think
-> Meanwhile the main script is running in an endless loop of the dots
kimconnect
Hi Marc,
I appreciate the opportunity to work with you on this. It appears that the script has ‘almost’ worked. It is suppose to detect whether the target is pending reboot and do that. However, that hasn’t happened for some reasons. I hope that it’s not firewall related as the computer and login credential where you’re triggering the script from (JumpBox) needs to be able to access the target’s \\$computer\c$\WindowsUpdate.log. If not, then I need to think of another way of detecting progress and set script to perform reboots and re-trigger updates…
I’ve created another link to simply this script for the special situation here: https://blog.kimconnect.com/update-windows-with-restricted-internet-access/
Let’s continue our development talks there.
Thanks,
KimConnect
Marc Hagemann
Yeah. The thing with the access to the logfile could be the reason. When I run the schedulked task script manually, this happens:
12/15/2020 11:23:23 => TESTWINDOWS has 0 available updates. Patching FINISHED
Add-Content : Access to the path ‘C:\WindowsUpdate.log’ is denied.
At C:\Scripts\WindowsUpdates.ps1:19 char:17
+ Add-Content -Path $logfile -Value $status
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : PermissionDenied: (C:\WindowsUpdate.log:String) [Add-Content], UnauthorizedAccessException
+ FullyQualifiedErrorId : GetContentWriterUnauthorizedAccessError,Microsoft.PowerShell.Commands.AddContentCommand
Don’t know, why access is denied to the Logfile, because the User I try with, has local admin rights
So I’ll try the new scipt, you created.
kimconnect
Use the new script (https://blog.kimconnect.com/update-windows-with-restricted-internet-access/) as it has a way to work with just WinRm
Anthony Castaneda
Awesome work- I’m getting an error when running this on PowerShell 5.1 and 7-
Attempting to use psexec to enable WinRM remotely…
Exception calling “Invoke” with “3” argument(s): “The running command stopped because the preference variable “ErrorActionPreference” or common parameter is set to Stop: The handle is invalid.”
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : ActionPreferenceStopException
+ PSComputerName : localhost
I’ve run as a domain admin as well as a local admin without much luck.
kimconnect
It appears that WinRM is unreachable at the Destination. Could be a firewall issue or WinRM isn’t setup/listening on remote machine. How about running this in a console or RDP session on that machine to see if it would make a difference: C:\Windows\system32\winrm.cmd qc -quiet
kimconnect
Wait, I notice that you’re running this script to update ‘localhost’… Well, this version is meant to update remote machines. For localhost, try this: https://blog.kimconnect.com/powershell-update-local-windows-using-com-objects-legacy-compatible/