Forked from ceth-x86/building-patch-report-for-windows.ps1
Created
July 5, 2021 17:47
-
-
Save itsmenaga/0d3b12fe854d176e7aa858e0e092e567 to your computer and use it in GitHub Desktop.
Managing Windows patches
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<# | |
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<# | |
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
## 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