Skip to content

Instantly share code, notes, and snippets.

@ceth-x86
Last active July 5, 2021 17:47
Show Gist options
  • Save ceth-x86/8f527ff7d6c83af160662a5e78b4085b to your computer and use it in GitHub Desktop.
Save ceth-x86/8f527ff7d6c83af160662a5e78b4085b to your computer and use it in GitHub Desktop.

Revisions

  1. ceth-x86 revised this gist Apr 20, 2019. 3 changed files with 686 additions and 0 deletions.
    92 changes: 92 additions & 0 deletions building-patch-report-for-windows.ps1
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,92 @@
    # 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'
    174 changes: 174 additions & 0 deletions creating-scheduled-tasks.ps1
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,174 @@
    <#
    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
    420 changes: 420 additions & 0 deletions deploying-patches.ps1
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,420 @@
    <#
    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
  2. ceth-x86 created this gist Apr 20, 2019.
    301 changes: 301 additions & 0 deletions query-installed-and-missing-pathces.ps1
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,301 @@
    ## 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