├── .gitattributes ├── PSWinUpdate.psd1 └── PSWinUpdate.psm1 /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /PSWinUpdate.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | RootModule = 'PSWinUpdate.psm1' 3 | ModuleVersion = '1.0.2' 4 | GUID = '99d04fde-6c95-42c5-88dd-6e2c511b369e' 5 | Author = 'Adam Bertram' 6 | Description = 'This module contains commands to both query and install Windows updates on remote Windows computers.' 7 | CompanyName = 'Adam the Automator, LLC.' 8 | PowerShellVersion = '4.0' 9 | FunctionsToExport = @('Get-WindowsUpdate', 'Install-WindowsUpdate') 10 | PrivateData = @{ 11 | PSData = @{ 12 | Tags = @('WindowsUpdate') 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /PSWinUpdate.psm1: -------------------------------------------------------------------------------- 1 | Set-StrictMode -Version Latest 2 | 3 | function Get-RemotingParameter { 4 | [OutputType([hashtable])] 5 | [CmdletBinding()] 6 | param ( 7 | [Parameter()] 8 | [string]$ComputerName, 9 | 10 | [Parameter()] 11 | [pscredential]$Credential 12 | ) 13 | 14 | $params = @{ 15 | ComputerName = $ComputerName 16 | } 17 | if ($Credential) { 18 | $params.Credential = $Credential 19 | } 20 | $params 21 | } 22 | 23 | function Remove-ScheduledTask { 24 | <# 25 | .SYNOPSIS 26 | This function looks for a scheduled task on a remote system and, once found, removes it. 27 | 28 | .EXAMPLE 29 | PS> Remove-ScheduledTask -ComputerName FOO -Name Task1 30 | 31 | .PARAMETER ComputerName 32 | A mandatory string parameter representing a FQDN of a remote computer. 33 | 34 | .PARAMETER Name 35 | A mandatory string parameter representing the name of the scheduled task. Scheduled tasks can be retrieved 36 | by using the Get-ScheduledTask cmdlet. 37 | 38 | .PARAMETER Credential 39 | Specifies a user account that has permission to perform this action. The default is the current user. 40 | 41 | Type a user name, such as 'User01' or 'Domain01\User01', or enter a variable that contains a PSCredential 42 | object, such as one generated by the Get-Credential cmdlet. When you type a user name, you will be prompted for a password. 43 | 44 | #> 45 | [OutputType([void])] 46 | [CmdletBinding(SupportsShouldProcess)] 47 | param 48 | ( 49 | [Parameter(Mandatory)] 50 | [ValidateNotNullOrEmpty()] 51 | [string]$ComputerName, 52 | 53 | [Parameter(Mandatory)] 54 | [ValidateNotNullOrEmpty()] 55 | [string]$Name, 56 | 57 | [Parameter()] 58 | [ValidateNotNullOrEmpty()] 59 | [pscredential]$Credential 60 | ) 61 | begin { 62 | $ErrorActionPreference = 'Stop' 63 | } 64 | process { 65 | try { 66 | $icmParams = Get-RemotingParameter -ComputerName $ComputerName -Credential $Credential 67 | $icmParams.ArgumentList = $Name 68 | $icmParams.ErrorAction = 'Ignore' 69 | 70 | $sb = { 71 | $taskName = "\$($args[0])" 72 | if (schtasks /query /TN $taskName) { 73 | schtasks /delete /TN $taskName /F 74 | } 75 | } 76 | 77 | if ($PSCmdlet.ShouldProcess("Remove scheduled task [$($Name)] from [$($ComputerName)]", '----------------------')) { 78 | Invoke-Command @icmParams -ScriptBlock $sb 79 | } 80 | } catch { 81 | throw $_.Exception.Message 82 | } 83 | } 84 | } 85 | 86 | function Wait-ScheduledTask { 87 | <# 88 | .SYNOPSIS 89 | This function looks for a scheduled task on a remote system and, once found, checks to see if it's running. 90 | If so, it will wait until the task has completed and return control. 91 | 92 | .EXAMPLE 93 | PS> Wait-ScheduledTask -ComputerName FOO -Name Task1 -Timeout 120 94 | 95 | .PARAMETER ComputerName 96 | A mandatory string parameter representing a FQDN of a remote computer. 97 | 98 | .PARAMETER Name 99 | A mandatory string parameter representing the name of the scheduled task. Scheduled tasks can be retrieved 100 | by using the Get-ScheduledTask cmdlet. 101 | 102 | .PARAMETER Timeout 103 | A optional integer parameter representing how long to wait for the scheduled task to complete. By default, 104 | it will wait 60 seconds. 105 | 106 | .PARAMETER Credential 107 | Specifies a user account that has permission to perform this action. The default is the current user. 108 | 109 | Type a user name, such as 'User01' or 'Domain01\User01', or enter a variable that contains a PSCredential 110 | object, such as one generated by the Get-Credential cmdlet. When you type a user name, you will be prompted for a password. 111 | 112 | #> 113 | [OutputType([void])] 114 | [CmdletBinding()] 115 | param 116 | ( 117 | [Parameter(Mandatory)] 118 | [ValidateNotNullOrEmpty()] 119 | [ValidateScript({Test-IsValidFqdn $_})] 120 | [string]$ComputerName, 121 | 122 | [Parameter(Mandatory)] 123 | [ValidateNotNullOrEmpty()] 124 | [string]$Name, 125 | 126 | [Parameter()] 127 | [ValidateNotNullOrEmpty()] 128 | [int]$Timeout = 300, ## seconds 129 | 130 | [Parameter()] 131 | [ValidateNotNullOrEmpty()] 132 | [pscredential]$Credential 133 | ) 134 | begin { 135 | $ErrorActionPreference = 'Stop' 136 | } 137 | process { 138 | try { 139 | $sessParams = Get-RemotingParameter -ComputerName $ComputerName -Credential $Credential 140 | $session = New-PSSession @sessParams 141 | 142 | $scriptBlock = { 143 | $taskName = "\$($args[0])" 144 | $VerbosePreference = 'Continue' 145 | $timer = [Diagnostics.Stopwatch]::StartNew() 146 | while (((schtasks /query /TN $taskName /FO CSV /v | ConvertFrom-Csv).Status -ne 'Ready') -and ($timer.Elapsed.TotalSeconds -lt $args[1])) { 147 | Write-Verbose -Message "Waiting on scheduled task [$taskName]..." 148 | Start-Sleep -Seconds 3 149 | } 150 | $timer.Stop() 151 | Write-Verbose -Message "We waited [$($timer.Elapsed.TotalSeconds)] seconds on the task [$taskName]" 152 | } 153 | 154 | Invoke-Command -Session $session -ScriptBlock $scriptBlock -ArgumentList $Name, $Timeout 155 | } catch { 156 | throw $_.Exception.Message 157 | } finally { 158 | if (Test-Path Variable:\session) { 159 | $session | Remove-PSSession 160 | } 161 | } 162 | } 163 | } 164 | 165 | function Get-WindowsUpdate { 166 | <# 167 | .SYNOPSIS 168 | This function retrieves a list of Microsoft updates based on a number of different criteria for a remote 169 | computer. It will retrieve these updates over a PowerShell remoting session. It uses the update source set 170 | at the time of query. If it's set to WSUS, it will only return updates that are advertised to the computer 171 | by WSUS. 172 | 173 | .EXAMPLE 174 | PS> Get-WindowsUpdate -ComputerName FOO 175 | 176 | .PARAMETER ComputerName 177 | A mandatory string parameter representing the FQDN of a computer. This is only mandatory is Session is 178 | not used. 179 | 180 | .PARAMETER Credential 181 | A optoional pscredential parameter representing an alternate credential to connect to the remote computer. 182 | 183 | .PARAMETER Session 184 | A mandatory PSSession parameter representing a PowerShell remoting session created with New-PSSession. This 185 | is only mandatory if ComputerName is not used. 186 | 187 | .PARAMETER Installed 188 | A optional boolean parameter set to either $true or $false depending on if you'd like to filter the resulting 189 | updates on this criteria. 190 | 191 | .PARAMETER Hidden 192 | A optional boolean parameter set to either $true or $false depending on if you'd like to filter the resulting 193 | updates on this criteria. 194 | 195 | .PARAMETER Assigned 196 | A optional boolean parameter set to either $true or $false depending on if you'd like to filter the resulting 197 | updates on this criteria. 198 | 199 | .PARAMETER RebootRequired 200 | A optional boolean parameter set to either $true or $false depending on if you'd like to filter the resulting 201 | updates on this criteria. 202 | #> 203 | [OutputType([System.Management.Automation.PSObject])] 204 | [CmdletBinding()] 205 | param 206 | ( 207 | [Parameter(Mandatory, ParameterSetName = 'ByComputerName')] 208 | [ValidateNotNullOrEmpty()] 209 | 210 | [string]$ComputerName, 211 | 212 | [Parameter(ParameterSetName = 'ByComputerName')] 213 | [ValidateNotNullOrEmpty()] 214 | [pscredential]$Credential, 215 | 216 | [Parameter(Mandatory, ParameterSetName = 'BySession')] 217 | [ValidateNotNullOrEmpty()] 218 | [System.Management.Automation.Runspaces.PSSession]$Session, 219 | 220 | [Parameter()] 221 | [ValidateNotNullOrEmpty()] 222 | [bool]$Installed = $false, 223 | 224 | [Parameter()] 225 | [ValidateNotNullOrEmpty()] 226 | [bool]$Hidden, 227 | 228 | [Parameter()] 229 | [ValidateNotNullOrEmpty()] 230 | [bool]$Assigned, 231 | 232 | [Parameter()] 233 | [ValidateNotNullOrEmpty()] 234 | [bool]$RebootRequired 235 | ) 236 | begin { 237 | $ErrorActionPreference = 'Stop' 238 | if (-not $Session) { 239 | Write-Verbose -Message 'Building new session...' 240 | $sessParams = Get-RemotingParameter -ComputerName $ComputerName -Credential $Credential 241 | $Session = New-PSSession @sessParams 242 | } 243 | } 244 | process { 245 | try { 246 | $criteriaParams = @{} 247 | 248 | @('Installed', 'Hidden', 'Assigned', 'RebootRequired').foreach({ 249 | if ($PSBoundParameters.ContainsKey($_)) { 250 | $criteriaParams[$_] = (Get-Variable -Name $_).Value 251 | } 252 | }) 253 | $query = NewUpdateCriteriaQuery @criteriaParams 254 | Write-Verbose -Message "Using the update criteria query: [$($Query)]..." 255 | SearchWindowsUpdate -Session $Session -Query $query 256 | } catch { 257 | throw $_.Exception.Message 258 | } finally { 259 | ## Only clean up the session if it was generated from within this function. This is because updates 260 | ## are stored in a variable to be used again by other functions, if necessary. 261 | if (($PSCmdlet.ParameterSetName -eq 'ByComputerName') -and (Test-Path Variable:\session)) { 262 | $session | Remove-PSSession 263 | } 264 | } 265 | } 266 | } 267 | 268 | function Install-WindowsUpdate { 269 | <# 270 | .SYNOPSIS 271 | This function retrieves all updates that are targeted at a remote computer, download and installs any that it 272 | finds. Depending on how the remote computer's update source is set, it will either read WSUS or Microsoft Update 273 | for a compliancy report. 274 | 275 | Once found, it will download each update, install them and then read output to detect if a reboot is required 276 | or not. 277 | 278 | .EXAMPLE 279 | PS> Install-WindowsUpdate -ComputerName FOO.domain.local 280 | 281 | .EXAMPLE 282 | PS> Install-WindowsUpdate -ComputerName FOO.domain.local,FOO2.domain.local 283 | 284 | .EXAMPLE 285 | PS> Install-WindowsUpdate -ComputerName FOO.domain.local,FOO2.domain.local -ForceReboot 286 | 287 | .PARAMETER ComputerName 288 | A mandatory string parameter representing one or more computer FQDNs. 289 | 290 | .PARAMETER Credential 291 | A optional pscredential parameter representing an alternate credential to connect to the remote computer. 292 | 293 | .PARAMETER ForceReboot 294 | An optional switch parameter to set if any updates on any computer targeted needs a reboot following update 295 | install. By default, computers are NOT rebooted automatically. Use this switch to force a reboot. 296 | 297 | .PARAMETER AsJob 298 | A optional switch parameter to set when activity needs to be sent to a background job. By default, this function 299 | waits for each computer to finish. However, if this parameter is used, it will start the process on each 300 | computer and immediately return a background job object to then monitor yourself with Get-Job. 301 | #> 302 | [OutputType([void])] 303 | [CmdletBinding()] 304 | param 305 | ( 306 | [Parameter(Mandatory)] 307 | [ValidateNotNullOrEmpty()] 308 | [string[]]$ComputerName, 309 | 310 | [Parameter()] 311 | [ValidateNotNullOrEmpty()] 312 | [pscredential]$Credential, 313 | 314 | [Parameter()] 315 | [ValidateNotNullOrEmpty()] 316 | [switch]$ForceReboot, 317 | 318 | [Parameter()] 319 | [ValidateNotNullOrEmpty()] 320 | [switch]$AsJob 321 | ) 322 | begin { 323 | $ErrorActionPreference = 'Stop' 324 | $scheduledTaskName = 'Windows Update Install' 325 | } 326 | process { 327 | try { 328 | @($ComputerName).foreach({ 329 | Write-Verbose -Message "Starting Windows update on [$($_)]" 330 | $installProcess = { 331 | param($ComputerName, $TaskName, $Credential, $ForceReboot) 332 | 333 | $ErrorActionPreference = 'Stop' 334 | try { 335 | if (-not (Get-WindowsUpdate -ComputerName $ComputerName)) { 336 | Write-Verbose -Message 'No updates needed to install. Skipping computer...' 337 | } else { 338 | $sessParams = @{ ComputerName = $ComputerName } 339 | if ($Credential) { 340 | $sessParams.Credential = $Credential 341 | } 342 | 343 | $session = New-PSSession @sessParams 344 | 345 | $scriptBlock = { 346 | $updateSession = New-Object -ComObject 'Microsoft.Update.Session'; 347 | $objSearcher = $updateSession.CreateUpdateSearcher(); 348 | if ($updates = ($objSearcher.Search('IsInstalled=0'))) { 349 | $updates = $updates.Updates; 350 | 351 | $downloader = $updateSession.CreateUpdateDownloader(); 352 | $downloader.Updates = $updates; 353 | $downloadResult = $downloader.Download(); 354 | if ($downloadResult.ResultCode -ne 2) { 355 | exit $downloadResult.ResultCode; 356 | } 357 | 358 | $installer = New-Object -ComObject Microsoft.Update.Installer; 359 | $installer.Updates = $updates; 360 | $installResult = $installer.Install(); 361 | if ($installResult.RebootRequired) { 362 | exit 7; 363 | } else { 364 | $installResult.ResultCode 365 | } 366 | } else { 367 | exit 6; 368 | } 369 | } 370 | 371 | $taskParams = @{ 372 | Session = $session 373 | Name = $TaskName 374 | Scriptblock = $scriptBlock 375 | } 376 | if ($Credential) { 377 | $taskParams.Credential = $args[2] 378 | } 379 | Write-Verbose -Message 'Creating scheduled task...' 380 | New-WindowsUpdateScheduledTask @taskParams 381 | 382 | Write-Verbose -Message "Starting scheduled task [$($TaskName)]..." 383 | 384 | $icmParams = @{ 385 | Session = $session 386 | ScriptBlock = { schtasks /run /TN "\$($args[0])" /I } 387 | ArgumentList = $TaskName 388 | } 389 | Invoke-Command @icmParams 390 | 391 | ## This could take awhile depending on the number of updates 392 | Wait-ScheduledTask -Name $TaskName -ComputerName $ComputerName -Timeout 2400 393 | 394 | $installResult = Get-WindowsUpdateInstallResult -Session $session 395 | 396 | if ($installResult -eq 'NoUpdatesNeeded') { 397 | Write-Verbose -Message "No updates to install" 398 | } elseif ($installResult -eq 'RebootRequired') { 399 | if ($ForceReboot) { 400 | Restart-Computer -ComputerName $ComputerName -Force -Wait; 401 | } else { 402 | Write-Warning "Reboot required but -ForceReboot was not used." 403 | } 404 | } else { 405 | throw "Updates failed. Reason: [$($installResult)]" 406 | } 407 | } 408 | } catch { 409 | Write-Error -Message $_.Exception.Message 410 | } finally { 411 | Remove-ScheduledTask -ComputerName $ComputerName -Name $TaskName 412 | } 413 | } 414 | 415 | $blockArgs = $_, $scheduledTaskName, $Credential, $ForceReboot.IsPresent 416 | if ($AsJob.IsPresent) { 417 | $jobParams = @{ 418 | ScriptBlock = $installProcess 419 | Name = "$_ - Windows Update Install" 420 | ArgumentList = $blockArgs 421 | InitializationScript = { Import-Module -Name 'GHI.Library.WindowsUpdate' } 422 | } 423 | Start-Job @jobParams 424 | } else { 425 | Invoke-Command -ScriptBlock $installProcess -ArgumentList $blockArgs 426 | } 427 | }) 428 | } catch { 429 | throw $_.Exception.Message 430 | } finally { 431 | if (-not $AsJob.IsPresent) { 432 | # Remove any sessions created. This is done when processes aren't invoked under a PS job 433 | Write-Verbose -Message 'Finding any lingering PS sessions on computers...' 434 | @(Get-PSSession -ComputerName $ComputerName).foreach({ 435 | Write-Verbose -Message "Removing PS session from [$($_)]..." 436 | Remove-PSSession -Session $_ 437 | }) 438 | } 439 | } 440 | } 441 | } 442 | 443 | function Get-WindowsUpdateInstallResult { 444 | [OutputType([string])] 445 | [CmdletBinding()] 446 | param 447 | ( 448 | [Parameter(Mandatory)] 449 | [System.Management.Automation.Runspaces.PSSession]$Session, 450 | 451 | [Parameter()] 452 | [ValidateNotNullOrEmpty()] 453 | [string]$ScheduledTaskName = 'Windows Update Install' 454 | ) 455 | 456 | $sb = { 457 | if ($result = schtasks /query /TN "\$($args[0])" /FO CSV /v | ConvertFrom-Csv) { 458 | $result.'Last Result' 459 | } 460 | } 461 | $resultCode = Invoke-Command -Session $Session -ScriptBlock $sb -ArgumentList $ScheduledTaskName 462 | switch -exact ($resultCode) { 463 | 0 { 464 | 'NotStarted' 465 | } 466 | 1 { 467 | 'InProgress' 468 | } 469 | 2 { 470 | 'Installed' 471 | } 472 | 3 { 473 | 'InstalledWithErrors' 474 | } 475 | 4 { 476 | 'Failed' 477 | } 478 | 5 { 479 | 'Aborted' 480 | } 481 | 6 { 482 | 'NoUpdatesNeeded' 483 | } 484 | 7 { 485 | 'RebootRequired' 486 | } 487 | default { 488 | "Unknown result code [$($_)]" 489 | } 490 | } 491 | } 492 | 493 | function NewUpdateCriteriaQuery { 494 | [OutputType([string])] 495 | [CmdletBinding()] 496 | param 497 | ( 498 | [Parameter()] 499 | [bool]$Installed, 500 | 501 | [Parameter()] 502 | [bool]$Hidden, 503 | 504 | [Parameter()] 505 | [bool]$Assigned, 506 | 507 | [Parameter()] 508 | [bool]$RebootRequired 509 | ) 510 | 511 | $conversion = @{ 512 | Installed = 'IsInstalled' 513 | Hidden = 'IsHidden' 514 | Assigned = 'IsAssigned' 515 | RebootRequired = 'RebootRequired' 516 | } 517 | 518 | $queryElements = @() 519 | $PSBoundParameters.GetEnumerator().where({ $_.Key -in $conversion.Keys }).foreach({ 520 | $queryElements += '{0}={1}' -f $conversion[$_.Key], [int]$_.Value 521 | }) 522 | $queryElements -join ' and ' 523 | } 524 | 525 | function SearchWindowsUpdate { 526 | [OutputType()] 527 | [CmdletBinding()] 528 | param 529 | ( 530 | [Parameter()] 531 | [string]$Query, 532 | 533 | [Parameter()] 534 | [System.Management.Automation.Runspaces.PSSession]$Session 535 | ) 536 | 537 | $scriptBlock = { 538 | $objSession = New-Object -ComObject 'Microsoft.Update.Session' 539 | $objSearcher = $objSession.CreateUpdateSearcher() 540 | if ($updates = ($objSearcher.Search($args[0]))) { 541 | $updates = $updates.Updates 542 | ## Save the updates needed to the file system for other functions to pick them up to download/install later. 543 | $updates | Export-CliXml -Path "$env:TEMP\Updates.xml" 544 | $updates 545 | } 546 | 547 | } 548 | Invoke-Command -Session $Session -ScriptBlock $scriptBlock -ArgumentList $Query 549 | } 550 | 551 | function New-WindowsUpdateScheduledTask { 552 | [OutputType([void])] 553 | [CmdletBinding()] 554 | param 555 | ( 556 | [Parameter(Mandatory)] 557 | [System.Management.Automation.Runspaces.PSSession]$Session, 558 | 559 | [Parameter(Mandatory)] 560 | [string]$Name, 561 | 562 | [Parameter(Mandatory)] 563 | [ValidateNotNullOrEmpty()] 564 | [scriptblock]$Scriptblock, 565 | 566 | [Parameter()] 567 | [ValidateNotNullOrEmpty()] 568 | [pscredential]$Credential 569 | ) 570 | 571 | $createStartSb = { 572 | $taskName = $args[0] 573 | $taskArgs = $args[1] -replace '"', '\"' 574 | $taskUser = $args[2] 575 | 576 | $tempScript = "$env:TEMP\WUUpdateScript.ps1" 577 | Set-Content -Path $tempScript -Value $taskArgs 578 | 579 | schtasks /create /SC ONSTART /TN $taskName /TR "powershell.exe -NonInteractive -NoProfile -File $tempScript" /F /RU $taskUser /RL HIGHEST 580 | } 581 | 582 | $command = $Scriptblock.ToString() 583 | 584 | $icmParams = @{ 585 | Session = $Session 586 | ScriptBlock = $createStartSb 587 | ArgumentList = $Name, $command 588 | } 589 | if ($PSBoundParameters.ContainsKey('Credential')) { 590 | $icmParams.ArgumentList += $Credential.UserName 591 | } else { 592 | $icmParams.ArgumentList += 'SYSTEM' 593 | } 594 | Write-Verbose -Message "Running code via powershell.exe: [$($command)]" 595 | Invoke-Command @icmParams 596 | 597 | } 598 | 599 | function Wait-WindowsUpdate { 600 | <# 601 | .SYNOPSIS 602 | This function looks for any currently running background jobs that were created by Install-WindowsUpdate 603 | and continually waits for all of them to finish before returning control to the console. 604 | 605 | .EXAMPLE 606 | PS> Wait-WindowsUpdate 607 | 608 | .PARAMETER Timeout 609 | An optional integer parameter representing the amount of seconds to wait for the job to finish. 610 | 611 | #> 612 | [OutputType([void])] 613 | [CmdletBinding()] 614 | param 615 | ( 616 | [Parameter()] 617 | [ValidateNotNullOrEmpty()] 618 | [int]$Timeout = 300 619 | ) 620 | process { 621 | try { 622 | if ($updateJobs = (Get-Job -Name '*Windows Update Install*').where({ $_.State -eq 'Running'})) { 623 | $timer = Start-Timer 624 | while ((Microsoft.PowerShell.Core\Get-Job -Id $updateJobs.Id | Where-Object { $_.State -eq 'Running' }) -and ($timer.Elapsed.TotalSeconds -lt $Timeout)) { 625 | Write-Verbose -Message "Waiting for all Windows Update install background jobs to complete..." 626 | Start-Sleep -Seconds 3 627 | } 628 | Stop-Timer -Timer $timer 629 | } 630 | } catch { 631 | throw $_.Exception.Message 632 | } 633 | } 634 | } --------------------------------------------------------------------------------