Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save itsmenaga/0d3b12fe854d176e7aa858e0e092e567 to your computer and use it in GitHub Desktop.
Save itsmenaga/0d3b12fe854d176e7aa858e0e092e567 to your computer and use it in GitHub Desktop.
Managing Windows patches
# Quickly allow filtering of the available updates by using the Out-GridView cmdlet
Import-Csv -Path 'C:\computers.txt' | Get-WindowsUpdate | Out-GridView
# Export the Results of Windows Update to a CSV File
Import-Csv -Path 'C:\computers.txt' | Get-WindowsUpdate | Export-CSV -Path '.\WindowsUpdate.csv' -NoTypeInformation -Force
Import-Csv -Path '.\WindowsUpdate.csv'
Function Out-WindowsUpdateReport {
<#
.SYNOPSIS
This function will output all piped in updates, remote or local, to an HTML page saved on disk.
.DESCRIPTION
Output the results of gathering Windows Updates to an HTML file on disk.
.EXAMPLE
PS> Get-WindowsUpdate | Out-WindowsUpdateReport
.PARAMETER FilePath
Location to output the report.
.PARAMETER UpdateResult
Updates to export.
#>
[OutputType('void')]
[CmdletBinding()]
Param(
[Parameter()]
[ValidateNotNullOrEmpty()]
[String]$FilePath = '.\WindowsUpdates.html',
[Parameter(Mandatory, ValueFromPipeline)]
[ValidateNotNullOrEmpty()]
[PSCustomObject]$UpdateResult
)
begin {
$ErrorActionPreference = 'Stop'
$header = @"
<!doctype html>
<html lang='en'>
<head>
<style type='text/css'>.updates{empty-cells:show;border:1px solid #cbcbcb;border-collapse:collapse;border-spacing:0}.updates thead{background-color:#e0e0e0;color:#000;text-align:left;vertical-align:bottom}.updates td,.updates th{padding:.5em 1em;border-width:0 0 1px;border-bottom:1px solid #cbcbcb;margin:0}.updates td:first-child,.updates th:first-child{border-left-width:0}.updates th{border-width:0 0 1px;border-bottom:1px solid #cbcbcb}.updates .installed{background-color:#a5d6a7;color:#030}.updates .notinstalled{background-color:#ef9a9a;color:#7f0000}</style>
</head>
<body>
<table class='updates'>
<thead>
<tr>
<th>Computer</th>
<th>KB ID</th>
<th>IsDownloaded</th>
<th>IsInstalled</th>
<th>RebootRequired</th>
</tr>
</thead>
<tbody>
"@
$body = ""
$footer = @"
</tbody>
</table>
</body>
</html>
"@
}
Process {
If ($UpdateResult.IsInstalled) {
$class = 'installed'
} Else {
$class = 'notinstalled'
}
$body += "`t`t`t<tr class='$class'><td>$($UpdateResult.ComputerName)</td><td>$($UpdateResult.'KB ID')</td><td>$($UpdateResult.IsDownloaded)</td><td>$($UpdateResult.IsInstalled)</td><td>$($UpdateResult.RebootRequired)</td></tr>`r`n"
}
End {
$html = $header + $body + $footer
$html | Out-File -FilePath $FilePath -Force
}
}
# Save the Results as an HTML Page
Get-WindowsUpdate | Out-WindowsUpdateReport
## Check the results of the report
Invoke-Item '.\WindowsUpdates.html'
# Save the Results as an HTML Page from a list of computers
Import-Csv -Path 'C:\computers.txt' | Get-WindowsUpdate | Out-WindowsUpdateReport
## Check the results of the report
Invoke-Item '.\WindowsUpdates.html'
<#
Scenario:
- Create a PowerShell script
- Create a scheduled task on a remote computer to execute PowerShell script
- Execute scheduled task
#>
#region Creating a local scheduled task to kick off a PowerShell script
## Creating a simple PowerShell script to create a file
$scriptPath = 'C:\CreateFile.ps1'
$testFilePath = 'C:\testing123.txt'
Add-Content -Path $scriptPath -Value "Add-Content -Path $testFilePath -Value 'created via PowerShell'"
Get-Content -Path $scriptPath
## What happens when the script is launched via calling PowerShell from cmd
powershell.exe -NonInteractive -NoProfile -File "$scriptPath"
## Creates the test file
Get-Content -Path $testFilePath
## Remove the test file to create it via the scheduled task
Remove-Item -Path $testFilePath
#endregion
#region Create a scheduled task to launch a script every day
## The test file doesn't exist
Test-Path -Path $testFilePath
$interval = 'Daily'
$time = '12:00'
$taskName = 'Testing123'
$taskUser = 'SYSTEM'
schtasks /create /SC $interval /ST $time /TN $taskName /TR "powershell.exe -NonInteractive -NoProfile -File `"$scriptPath`"" /F /RU $taskUser /RL HIGHEST
## Check out the task created and run it
control schedtasks
## The test file is back because the scheduled task launched the PowerShell script
Get-Content -Path $testFilePath
#endregion
#region Creating a remote scheduled task to kick off a PowerShell script
## We must wrap all of the code to run on the remote server in a scriptblock
$createStartSb = {
$interval = 'Daily'
$time = '12:00'
$taskName = 'Testing123'
$taskUser = 'SYSTEM'
## Create the PowerShell script which the scheduled task will execute
$scheduledTaskScriptFolder = 'C:\ScheduledTaskScripts'
if (-not (Test-Path -Path $scheduledTaskScriptFolder -PathType Container)) {
$null = New-Item -Path $scheduledTaskScriptFolder -ItemType Directory
}
$scriptPath = "$scheduledTaskScriptFolder\CreateScript.ps1"
Set-Content -Path $scriptPath -Value "Add-Content -Path 'C:\testing123.txt' -Value 'created via PowerShell'"
## Create the scheduled task
schtasks /create /SC $interval /ST $time /TN $taskName /TR "powershell.exe -NonInteractive -NoProfile -File `"$scriptPath`"" /F /RU $taskUser /RL HIGHEST
}
## Execute the code in the scriptblock on the remote computer
$scheduledTaskServer = 'DC'
$icmParams = @{
ComputerName = $scheduledTaskServer
ScriptBlock = $createStartSb
}
Invoke-Command @icmParams
## test file doesn't exist
Test-Path -Path "\\DC\c$\testing123.txt"
## Check out the task created and run it
control schedtasks
## The test file is back because the scheduled task launched the PowerShell script
Get-Content -Path "\\DC\c$\testing123.txt"
#endregion
#region Creating a scheduled task function
## This is where we "parameterize" creating a scheduled task on a remote computer by allowing dynamic
## input like scheduled task name, the contents of the PowerShell script, interval, time, etc. We pass
## in all of this information at run-time.
function New-PsScheduledTask {
[OutputType([void])]
[CmdletBinding()]
param
(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$ComputerName,
[Parameter(Mandatory)]
[string]$Name,
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[scriptblock]$Scriptblock,
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[ValidateSet('Daily', 'Weekly', 'Once')] ## This can be other intervals but we're limiting to just these for now
[string]$Interval,
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$Time,
[Parameter()]
[ValidateNotNullOrEmpty()]
[ValidateSet('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday')]
[string]$DayOfWeek,
[Parameter()]
[ValidateNotNullOrEmpty()]
[pscredential]$RunAsCredential
)
$createStartSb = {
param($taskName, $command, $interval, $time, $taskUser)
## Create the PowerShell script which the scheduled task will execute
$scheduledTaskScriptFolder = 'C:\ScheduledTaskScripts'
if (-not (Test-Path -Path $scheduledTaskScriptFolder -PathType Container)) {
$null = New-Item -Path $scheduledTaskScriptFolder -ItemType Directory
}
$scriptPath = "$scheduledTaskScriptFolder\$taskName.ps1"
Set-Content -Path $scriptPath -Value $command
## Create the scheduled task
schtasks /create /SC $interval /ST $time /TN `"$taskName`" /TR "powershell.exe -NonInteractive -NoProfile -File `"$scriptPath`"" /F /RU $taskUser /RL HIGHEST
}
$icmParams = @{
ComputerName = $ComputerName
ScriptBlock = $createStartSb
ArgumentList = $Name, $Scriptblock.ToString(), $Interval, $Time
}
if ($PSBoundParameters.ContainsKey('Credential')) {
$icmParams.ArgumentList += $RunAsCredential.UserName
} else {
$icmParams.ArgumentList += 'SYSTEM'
}
Invoke-Command @icmParams
}
$params = @{
ComputerName = 'DC'
Name = 'Testing123'
ScriptBlock = { Add-Content -Path 'C:\testing123.txt' -Value 'Created with PowerShell' }
Interval = 'Once'
Time = '1:00'
}
New-PsScheduledTask @params
## Start the scheduled task
Invoke-Command -ComputerName DC -ScriptBlock { Start-ScheduledTask -TaskName 'Testing123' }
control schedtasks
Get-Content -Path '\\DC\c$\testing123.txt'
#endregion
<#
Scenario:
- Find all missing updates
- Download missing updates
- Install the updates
- Create Install-WindowsUpdate function
#>
#region Download updates
## Let's first check for any missing updates. We have one that's not downloaded and installed
Get-WindowsUpdate
$updateSession = New-Object -ComObject 'Microsoft.Update.Session'
$updateSearcher = $updateSession.CreateUpdateSearcher()
# Create the update collection object to add our updates to
$updatesToDownload = New-Object -ComObject 'Microsoft.Update.UpdateColl'
$updates = $updateSearcher.Search($null)
# Filter out just the updates that we want and add them to our collection
$updates.updates | Foreach-Object { $updatesToDownload.Add($_) | Out-Null }
# Create the download object, assign our updates to download and initiate the download
$downloader = $updateSession.CreateUpdateDownloader()
$downloader.Updates = $updatesToDownload
$downloadResult = $downloader.Download()
# Show the updates to verify that they've been downloaded
Get-WindowsUpdate
#endregion
#region Install the updates locally
$updatesToInstall = New-Object -ComObject 'Microsoft.Update.UpdateColl'
$updates.updates |
Where-Object IsDownloaded -EQ $true |
Foreach-Object { $updatesToInstall.Add($_) | Out-Null }
# Create the installation object, assign our updates to download and initiate the download
$installer = New-Object -ComObject 'Microsoft.Update.Installer'
$installer.Updates = $updatesToInstall
$installResult = $installer.Install()
$installResult
## Check for missing updates again
Get-WindowsUpdate
#endregion
#region Install updates remotely but denied
$ComputerName = 'DC'
Get-WindowsUpdate -ComputerName $ComputerName
$scriptBlock = {
$updateSession = New-Object -ComObject 'Microsoft.Update.Session';
$objSearcher = $updateSession.CreateUpdateSearcher()
$updates = $objSearcher.Search('IsInstalled=0')
$updates = $updates.Updates
$downloader = $updateSession.CreateUpdateDownloader()
### Other code to download and install updates here ###
}
## Attempt this the "usual" way even if we're an admin on the remote computer, we'll get Access Denied
Invoke-Command -ComputerName $ComputerName -ScriptBlock $scriptBlock
#endregion
#endregion
function Install-WindowsUpdate {
<#
.SYNOPSIS
This function retrieves all updates that are targeted at a remote computer, download and installs any that it
finds. Depending on how the remote computer's update source is set, it will either read WSUS or Microsoft Update
for a compliancy report.
Once found, it will download each update, install them and then read output to detect if a reboot is required
or not.
.EXAMPLE
PS> Install-WindowsUpdate -ComputerName FOO.domain.local
.EXAMPLE
PS> Install-WindowsUpdate -ComputerName FOO.domain.local,FOO2.domain.local
.EXAMPLE
PS> Install-WindowsUpdate -ComputerName FOO.domain.local,FOO2.domain.local -ForceReboot
.PARAMETER ComputerName
A mandatory string parameter representing one or more computer FQDNs.
.PARAMETER Credential
A optional pscredential parameter representing an alternate credential to connect to the remote computer.
.PARAMETER ForceReboot
An optional switch parameter to set if any updates on any computer targeted needs a reboot following update
install. By default, computers are NOT rebooted automatically. Use this switch to force a reboot.
.PARAMETER AsJob
A optional switch parameter to set when activity needs to be sent to a background job. By default, this function
waits for each computer to finish. However, if this parameter is used, it will start the process on each
computer and immediately return a background job object to then monitor yourself with Get-Job.
#>
[OutputType([void])]
[CmdletBinding()]
param
(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string[]]$ComputerName,
[Parameter()]
[ValidateNotNullOrEmpty()]
[switch]$ForceReboot,
[Parameter()]
[ValidateNotNullOrEmpty()]
[switch]$AsJob
)
begin {
$ErrorActionPreference = 'Stop'
$scheduledTaskName = 'WindowsUpdateInstall'
}
process {
try {
@($ComputerName).foreach({
Write-Verbose -Message "Starting Windows update on [$($_)]"
## Create the scriptblock. This is only done in case the function
## needs to be executed via a background job. Otherwise, we wouldn't need to wrap
## this code in a scriptblock.
$installProcess = {
param($ComputerName, $TaskName, $ForceReboot)
$ErrorActionPreference = 'Stop'
try {
## Create a PSSession to reuse
$sessParams = @{ ComputerName = $ComputerName }
$session = New-PSSession @sessParams
## Create the scriptblock to pass to the remote computer
$scriptBlock = {
$updateSession = New-Object -ComObject 'Microsoft.Update.Session';
$objSearcher = $updateSession.CreateUpdateSearcher()
## Check for missing updates. Are updates needed?
$u = $objSearcher.Search('IsInstalled=0')
if ($u.updates) {
Add-Content -Path 'C:\foo.txt' -Value ($u.updates -eq $null)
$updates = $u.updates
## Download the updates
$downloader = $updateSession.CreateUpdateDownloader()
$downloader.Updates = $updates
$downloadResult = $downloader.Download()
## Check the download result and quit if it wasn't successful (2)
if ($downloadResult.ResultCode -ne 2) {
exit $downloadResult.ResultCode
}
## Install all of the updates we just downloaded
$installer = New-Object -ComObject Microsoft.Update.Installer
$installer.Updates = $updates
$installResult = $installer.Install()
## Exit with specific error codes
if ($installResult.RebootRequired) {
exit 7
} else {
$installResult.ResultCode
}
} else {
exit 6
}
}
Write-Verbose -Message 'Creating scheduled task...'
$params = @{
ComputerName = $ComputerName
Name = $TaskName
ScriptBlock = $scriptBlock
Interval = 'Once'
Time = '23:00' ## doesn't matter
}
New-PsScheduledTask @params
Write-Verbose -Message "Starting scheduled task [$($TaskName)]..."
$icmParams = @{
Session = $session
ScriptBlock = { Start-ScheduledTask -TaskName $args[0] }
ArgumentList = $TaskName
}
Invoke-Command @icmParams
## This could take awhile depending on the number of updates
Wait-ScheduledTask -Name $scheduledTaskName -ComputerName $ComputerName -Timeout 2400
## Parse the result in another function for modularity
$installResult = Get-WindowsUpdateInstallResult -Session $session -ScheduledTaskName $scheduledTaskName
if ($installResult -eq 'NoUpdatesNeeded') {
Write-Verbose -Message "No updates to install"
} elseif ($installResult -eq 'RebootRequired') {
if ($ForceReboot) {
Restart-Computer -ComputerName $ComputerName -Force -Wait;
} else {
Write-Warning "Reboot required but -ForceReboot was not used."
}
} else {
throw "Updates failed. Reason: [$($installResult)]"
}
} catch {
Write-Error -Message $_.Exception.Message
} finally {
## Remove the scheduled task because we just needed it to run our
## updates as SYSTEM
Remove-ScheduledTask -ComputerName $ComputerName -Name $scheduledTaskName
}
}
$blockArgs = $_, $scheduledTaskName, $Credential, $ForceReboot.IsPresent
if ($AsJob.IsPresent) {
$jobParams = @{
ScriptBlock = $installProcess
Name = "$_ - EO Windows Update Install"
ArgumentList = $blockArgs
InitializationScript = { Import-Module -Name 'GHI.Library.WindowsUpdate' }
}
Start-Job @jobParams
} else {
Invoke-Command -ScriptBlock $installProcess -ArgumentList $blockArgs
}
})
} catch {
throw $_.Exception.Message
} finally {
if (-not $AsJob.IsPresent) {
# Remove any sessions created. This is done when processes aren't invoked under a PS job
Write-Verbose -Message 'Finding any lingering PS sessions on computers...'
@(Get-PSSession -ComputerName $ComputerName).foreach({
Write-Verbose -Message "Removing PS session from [$($_)]..."
Remove-PSSession -Session $_
})
}
}
}
}
function Get-WindowsUpdateInstallResult {
[OutputType([string])]
[CmdletBinding()]
param
(
[Parameter(Mandatory)]
[System.Management.Automation.Runspaces.PSSession]$Session,
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$ScheduledTaskName
)
$sb = {
if ($result = schtasks /query /TN "\$($args[0])" /FO CSV /v | ConvertFrom-Csv) {
$result.'Last Result'
}
}
$resultCode = Invoke-Command -Session $Session -ScriptBlock $sb -ArgumentList $ScheduledTaskName
switch -exact ($resultCode) {
0 {
'NotStarted'
}
1 {
'InProgress'
}
2 {
'Installed'
}
3 {
'InstalledWithErrors'
}
4 {
'Failed'
}
5 {
'Aborted'
}
6 {
'NoUpdatesNeeded'
}
7 {
'RebootRequired'
}
default {
"Unknown result code [$($_)]"
}
}
}
function Remove-ScheduledTask {
<#
.SYNOPSIS
This function looks for a scheduled task on a remote system and, once found, removes it.
.EXAMPLE
PS> Remove-ScheduledTask -ComputerName FOO -Name Task1
.PARAMETER ComputerName
A mandatory string parameter representing a FQDN of a remote computer.
.PARAMETER Name
A mandatory string parameter representing the name of the scheduled task. Scheduled tasks can be retrieved
by using the Get-ScheduledTask cmdlet.
#>
[OutputType([void])]
[CmdletBinding(SupportsShouldProcess)]
param
(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$ComputerName,
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$Name
)
process {
try {
$icmParams = @{ 'ComputerName' = $ComputerName }
$icmParams.ArgumentList = $Name
$icmParams.ErrorAction = 'Ignore'
$sb = {
$taskName = "\$($args[0])"
if (schtasks /query /TN $taskName) {
schtasks /delete /TN $taskName /F
}
}
if ($PSCmdlet.ShouldProcess("Remove scheduled task [$($Name)] from [$($ComputerName)]", '----------------------')) {
Invoke-Command @icmParams -ScriptBlock $sb
}
} catch {
throw $_.Exception.Message
}
}
}
function Wait-ScheduledTask {
<#
.SYNOPSIS
This function looks for a scheduled task on a remote system and, once found, checks to see if it's running.
If so, it will wait until the task has completed and return control.
.EXAMPLE
PS> Wait-ScheduledTask -ComputerName FOO -Name Task1 -Timeout 120
.PARAMETER ComputerName
A mandatory string parameter representing a FQDN of a remote computer.
.PARAMETER Name
A mandatory string parameter representing the name of the scheduled task. Scheduled tasks can be retrieved
by using the Get-ScheduledTask cmdlet.
.PARAMETER Timeout
A optional integer parameter representing how long to wait for the scheduled task to complete. By default,
it will wait 60 seconds.
.PARAMETER Credential
Specifies a user account that has permission to perform this action. The default is the current user.
Type a user name, such as 'User01' or 'Domain01\User01', or enter a variable that contains a PSCredential
object, such as one generated by the Get-Credential cmdlet. When you type a user name, you will be prompted for a password.
#>
[OutputType([void])]
[CmdletBinding()]
param
(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$ComputerName,
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$Name,
[Parameter()]
[ValidateNotNullOrEmpty()]
[int]$Timeout = 300 ## seconds
)
process {
try {
$session = New-PSSession -ComputerName $ComputerName
$scriptBlock = {
$taskName = "\$($args[0])"
$VerbosePreference = 'Continue'
$timer = [Diagnostics.Stopwatch]::StartNew()
while (((schtasks /query /TN $taskName /FO CSV /v | ConvertFrom-Csv).Status -ne 'Ready') -and ($timer.Elapsed.TotalSeconds -lt $args[1])) {
Write-Verbose -Message "Waiting on scheduled task [$taskName]..."
Start-Sleep -Seconds 3
}
$timer.Stop()
Write-Verbose -Message "We waited [$($timer.Elapsed.TotalSeconds)] seconds on the task [$taskName]"
}
Invoke-Command -Session $session -ScriptBlock $scriptBlock -ArgumentList $Name, $Timeout
} catch {
throw $_.Exception.Message
} finally {
if (Test-Path Variable:\session) {
$session | Remove-PSSession
}
}
}
}
Install-WindowsUpdate -ComputerName DC -Verbose
#endregion
## Scenario: Query updates different ways on a local and remote computer
#region Explain the entire process without the function all in one go in it's simplest form
$updateSession = New-Object -ComObject 'Microsoft.Update.Session'
$updateSearcher = $updateSession.CreateUpdateSearcher()
$query = 'IsInstalled=0'
$updates = ($updateSearcher.Search($query))
## Show there's not much good output here
$updates
## Need to drill down into the updates property
$updates.Updates
## Limit to only interesting data
$updates.Updates | Select-Object Title, LastDeploymentChangeTime, Description, RebootRequired, IsDownloaded, IsHidden
#endregion
#region Hit another use case or two trying not to repeat much from above
## Find Updates Required a Reboot
$Updates = $UpdateSearcher.Search('RebootRequired=1')
$Updates.Updates | Select-Object Title, Description, RebootRequired, IsDownloaded, IsHidden
## Multiple Conditions
$Updates = $UpdateSearcher.Search('IsInstalled=0 AND RebootRequired=1')
$Updates.Updates | Select-Object Title, Description, RebootRequired, IsDownloaded, IsHidden
#endregion
#region Using Get-HotFix
# Retrieves hotfixes (updates) that have been installed by Windows Update, Microsoft Update, Windows Server Updates
# Pulls data from the WMI class: Win32_QuickFixEngineering
# This class only reutnrs updates supplied by Compoonent Based Servicing (CBS). Updates supplied by MSI or the Windows Update Site are not returned.
Get-HotFix
Get-HotFix -ComputerName 'DC'
#endregion
#region Search by Category
$UpdateObjectSearcher = New-Object -ComObject 'Microsoft.Update.Searcher'
$InstalledUpdates = $UpdateObjectSearcher.Search("IsInstalled=1")
$InstalledUpdates.Updates | Where-Object { 'Security Updates' -in ($_.Categories | foreach { $_.Name }) } | Select-Object Title, LastDeploymentChangeTime
#endregion
## Other query options
## RebootRequired=1, IsHidden=1, IsAssigned=1, IsInstalled=0 AND RebootRequired=1
#region Get Updates on a Remote Computer (PSRemoting)
$scriptblock = {
$UpdateObjectSession = New-Object -ComObject 'Microsoft.Update.Session'
$UpdateSearcher = $UpdateObjectSession.CreateUpdateSearcher()
$Updates = $UpdateSearcher.Search($null)
$Updates.Updates | Select-Object Title, Description, RebootRequired, IsDownloaded, IsHidden
}
Invoke-Command -ComputerName 'DC' -ScriptBlock $scriptblock
#endregion
#region Remotely Trigger Update Detection (wuauclt /detectnow)
$scriptblock = {
$AutoUpdate = New-Object -ComObject 'Microsoft.Update.AutoUpdate'
$AutoUpdate.DetectNow()
}
Invoke-Command -ComputerName 'DC' -ScriptBlock $scriptblock
$scriptblock = {
$AutoUpdate = New-Object -ComObject 'Microsoft.Update.AutoUpdate'
$AutoUpdate.Results
}
Invoke-Command -ComputerName 'DC' -ScriptBlock $scriptblock
#endregion
#region Microsoft Update, Windows Update and WSUS
# Microsoft Updates (normally the default) is MS product updates and everything in Windows Updates
# Windows Updates are Service Packs and core upates but not product updates
$serviceManager = New-Object -Com 'Microsoft.Update.ServiceManager'
$serviceManager.Services | Select-Object Name, ISManaged, IsDefaultAUService, ServiceUrl
#endregion
#region Running as a Job
$scriptBlock = {
$updateSession = New-Object -ComObject 'Microsoft.Update.Session'
$updateSearcher = $updateSession.CreateUpdateSearcher()
If ($updates = ($updateSearcher.Search($Null))) {
$updates.Updates
}
}
$Params = @{
"ComputerName" = 'DC'
"ScriptBlock" = $scriptBlock
"AsJob" = $true
"JobName" = 'DC - Windows Update Query'
}
$null = Invoke-Command @Params
Get-Job -Name 'DC - Windows Update Query' | Wait-Job | Receive-Job
#endregion
#region Parallel Computers
# Clear all previous jobs
Get-Job | Remove-Job
$Computers = @(
'DC'
'CLIENT2'
'WSUS'
'CLIENT3'
)
$Jobs = @()
$Results = @()
$scriptBlock = {
$updateSession = New-Object -ComObject 'Microsoft.Update.Session'
$updateSearcher = $updateSession.CreateUpdateSearcher()
If ($updates = ($updateSearcher.Search($Null))) {
$updates.Updates
}
}
$Computers | Foreach-Object {
# Not all computers are ICMP ping enabled, but do support PSRemote which is what we need
Try {
Test-WSMan -ComputerName $_ -ErrorAction Stop | Out-Null
} Catch {
Return
}
$Name = "$($_) - Windows Update Query"
$Params = @{
"ComputerName" = $_
"ScriptBlock" = $scriptBlock
"AsJob" = $true
"JobName" = $Name
}
Try {
$null = Invoke-Command @Params
} Catch {
Throw $_.Exception.Message
}
$Jobs += Get-Job -Name $Name
}
$Jobs | Wait-Job | Receive-Job | Foreach-Object { $Results += $_ }
$Results | Select-Object PSComputerName, Title | Format-Table -AutoSize
#endregion
#region Wrap it all up into a function
Function Get-WindowsUpdate {
<#
.SYNOPSIS
This function retrieves all Windows Updates meeting the given criteria locally or remotely.
.DESCRIPTION
Utilizing the built-in Windows COM objects to interact with the Windows Update service retrieve all Windows Updates meeting the given criteria both on the local system or on a remote system.
.EXAMPLE
PS> Get-WindowsUpdate
Title LastDeploymentChangeTime
----- -------------------
Windows Malicious Software Removal Tool x64 - February 2019 (KB890830) 2/13/2019 12:00:...
2019-02 Cumulative Update for .NET Framework 3.5 and 4.7.2 for Windows 10 Version 1809 for x64 (KB4483452) 2/13/2019 12:00:...
2019-02 Cumulative Update for Windows 10 Version 1809 for x64-based Systems (KB4487044) 2/13/2019 12:00:...
.PARAMETER Installed
Return installed updates.
.PARAMETER Hidden
Return updates that have been hidden from installation.
.PARAMETER Assigned
Return updates that are intended for deployment by Windows Automatic Updates.
.PARAMETER RebootRequired
Return updates that require a reboot after installation.
.PARAMETER ComputerName
The remote system to retrieve updates from, also aliased as 'Name'.
#>
[OutputType([PSCustomObject])]
[CmdletBinding()]
Param (
[Bool]$Installed,
[Bool]$Hidden,
[Bool]$Assigned,
[Bool]$RebootRequired,
[Parameter(ValueFromPipelineByPropertyName)]
[Alias('Name')]
[String]$ComputerName,
[Switch]$AsJob
)
Begin {
## Create a hashtable to easily "convert" the function paramters to query parts.
$paramToQueryMap = @{
Installed = 'IsInstalled'
Hidden = 'IsHidden'
Assigned = 'IsAssigned'
RebootRequired = 'RebootRequired'
}
$query = @()
## Build the query string
$paramToQueryMap.GetEnumerator() | Foreach-Object {
If ($PSBoundParameters.ContainsKey($_.Name)) {
$query += '{0}={1}' -f $paramToQueryMap[$_.Name], [Int](Get-Variable -Name $_.Name).Value
}
}
$query = $query -Join ' AND '
}
Process {
Try {
## Create the scriptblock we'll use to pass to the remote computer or run locally
Write-Verbose -Message "Checking for updates on [$($ComputerName)]..."
$scriptBlock = {
param ($Query)
Write-Verbose "Query is '$Query'"
$updateSession = New-Object -ComObject 'Microsoft.Update.Session'
$updateSearcher = $updateSession.CreateUpdateSearcher()
If ($result = $updateSearcher.Search($Query)) {
if ($result.Updates.Count -gt 0) {
$result.Updates | foreach {
$update = $_
$properties = @(
@{ 'Name' = 'IsDownloaded'; Expression = { $update.IsDownloaded }}
@{ 'Name' = 'IsInstalled'; Expression = { $update.IsInstalled }}
@{ 'Name' = 'RebootRequired'; Expression = { $update.RebootRequired }}
@{ 'Name' = 'ComputerName'; Expression = { $env:COMPUTERNAME }}
@{ 'Name' = 'KB ID'; Expression = { $_.replace('KB', '') }}
)
$_.KBArticleIds | Select-Object -Property $properties
}
}
}
if ($Query -eq 'IsInstalled=1') {
$properties = @(
@{ 'Name' = 'IsDownloaded'; Expression = { $true }}
@{ 'Name' = 'IsInstalled'; Expression = { $true }}
@{ 'Name' = 'RebootRequired'; Expression = { 'Unknown' }}
@{ 'Name' = 'ComputerName'; Expression = { $env:COMPUTERNAME }}
@{ 'Name' = 'KB ID'; Expression = { $_.replace('KB', '') }}
)
(Get-Hotfix).HotFixId | Select-Object -Property $properties
}
}
## Run the query
$icmParams = @{
'ScriptBlock' = $scriptBlock
'ArgumentList' = $Query
}
if ($PSBoundParameters.ContainsKey('AsJob')) {
if (-not $PSBoundParameters.ContainsKey('ComputerName')) {
throw 'This function cannot run as a job on the local comoputer.'
} else {
$icmParams.JobName = $ComputerName
$icmParams.AsJob = $true
}
}
if ($PSBoundParameters.ContainsKey('ComputerName')) {
$icmParams.ComputerName = $ComputerName
$icmParams.HideComputerName = $true
}
Invoke-Command @icmParams | Select-Object -Property * -ExcludeProperty 'RunspaceId'
} Catch {
Throw $_.Exception.Message
}
}
}
#endregion
## Function demonstration
Get-WindowsUpdate
Get-WindowsUpdate -ComputerName 'DC'
Get-WindowsUpdate -ComputerName 'DC' -Installed $true
Import-Csv -Path 'C:\computers.txt'
Import-Csv -Path 'C:\computers.txt' | Get-WindowsUpdate
Get-Job | Remove-Job
Import-Csv -Path 'C:\computers.txt' | Get-WindowsUpdate -AsJob
Get-Job
Get-Job | Receive-Job
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment