zWUScript.ps1

### Module - zWindowsUpdate

<#
.NOTES
    ######################
     mail@nimbus117.co.uk
    ######################
.SYNOPSIS
    Search for, download and install windows updates.
.DESCRIPTION
    By default this script will search for, download and install all software updates but will not reboot the computer if required. Optionally Search for specific update types, such as CriticalUpdates or SecurityUpdates, exclude optional updates, automatically reboot if required, search for updates only or download updates without installing them. All output is written to an .xml logfile in the directory the script is run, you can use Import-Clixml to view the log. There is no output to the host unless you use the -Verbose parameter.
.PARAMETER AcceptEula
    Accept update EULA if needed.
.PARAMETER AutoSelect
    Only include updates that are flagged to be automatically selected by Windows Update.
.PARAMETER DownloadOnly
    Download updates but do not install them.
.PARAMETER ExcludeKB
    Exclude updates by KB number.
.PARAMETER ExcludeOptional
    Exclude updates that are considered optional.
.PARAMETER IncludeKB
    Include updates by KB number.
.PARAMETER Reboot
    Attempt to reboot the computer if required after installing updates.
.PARAMETER SearchOnly
    Search for updates only, do not download or install them.
.PARAMETER Service
    Select update service. Possible values are 'MicrosoftUpdate', 'WindowsUpdate', 'WSUS'. When not specified the system default is used. The script will attempt to add the MicrosoftUpdate service if it is not registered.
.PARAMETER SmtpFrom
    From address for the email report.
.PARAMETER SmtpServer
    Smtp server used to send the email report.
.PARAMETER SmtpTo
    To address for the email report.
.PARAMETER UpdateType
    Specify which update types to search for, such as CriticalUpdates or SecurityUpdates. Possible values are 'Application', 'CriticalUpdates', 'Definitions', 'FeaturePacks', 'SecurityUpdates', 'ServicePacks', 'Tools', 'UpdateRollups', 'Updates'. The default is all software updates.
.EXAMPLE
    PS C:\> .\zWUScript.ps1 -UpdateType CriticalUpdates,SecurityUpdates -SearchOnly
    Search for needed critical and security updates.
.EXAMPLE
    PS C:\> .\zWUScript.ps1 -AutoSelect -Reboot -SmtpFrom zWU@domain.com -SmtpTo john@domain.com -SmtpServer mail.domain.com
    Search for, download and install automatically selected updates. The computer will be restarted automatically if required and a report email will be sent.
#>


#Requires -Version 4.0

[cmdletbinding(DefaultParameterSetName='Install')]

param( 

    [parameter(ParameterSetName = 'DownloadOnly')]
    [parameter(ParameterSetName = 'Install')]
    [switch]$AcceptEula,

    [switch]$AutoSelect,

    [parameter(ParameterSetName = 'DownloadOnly')]
    [switch]$DownloadOnly,

    [ValidateScript({$_ -match 'kb[0-9]{6,}'})]
    [string[]]$ExcludeKB,

    [switch]$ExcludeOptional,

    [ValidateScript({$_ -match 'kb[0-9]{6,}'})]
    [string[]]$IncludeKB,

    [parameter(ParameterSetName = 'Install')]
    [switch]$Reboot,

    [parameter(ParameterSetName = 'SearchOnly')]
    [switch]$SearchOnly,

    [ValidateSet('MicrosoftUpdate', 'WindowsUpdate', 'WSUS')]
    [ValidateNotNullOrEmpty()]
    [string]$Service,
    
    [Net.Mail.MailAddress]$SmtpFrom,

    [ValidateNotNullOrEmpty()]
    [string]$SmtpServer,

    [Net.Mail.MailAddress]$SmtpTo,

    [ValidateSet('Application', 'CriticalUpdates', 'Definitions', 'FeaturePacks', 'SecurityUpdates', 'ServicePacks', 'Tools', 'UpdateRollups', 'Updates')]
    [ValidateNotNullOrEmpty()]
    [string[]]$UpdateType
)

if ($SmtpFrom -and $SmtpTo -and $SmtpServer) {$SendEmail = $true}

elseif ($SmtpFrom -or $SmtpTo -or $SmtpServer) {throw "$(Get-Location)\$($MyInvocation.MyCommand.Name) : Provide all -Smtp* parameters"}

$LogPath = '.\zWUScriptLog.xml'

## Write to xml log

function WULog {

    param(
        
        [Parameter(Position = 0,Mandatory = $True)]
        [ValidateNotNullOrEmpty()]
        [string[]]$Message,

        [Parameter(Position=1)]
        [ValidateSet('Info', 'Warning', 'Error')]
        [string]$Level = 'Info',

        [switch]$Overwrite
    )

    $Date = Get-Date

    $Log = $Message | ForEach-Object {

        [PSCustomObject]@{Timestamp = $Date ; Level = $Level ; Message = $_}
        
        Write-Verbose "$Date - $Level - $_"
    }
    
    if (-not $Overwrite) {
        
        if (Test-Path -Path $LogPath -PathType Leaf) {

            $Import = @(Import-Clixml -Path $LogPath)

            $Log = $Import += $Log
        }
    }

    $Log | Export-Clixml -Path $LogPath -Force
}

## Convert units

function WUUnits {

param(
        [Parameter(Position = 0,Mandatory = $True)]
        [ValidateNotNullOrEmpty()]
        [double]$Bytes,

        [Parameter(Position=1)]
        [ValidateRange(0,5)]
        [int]$Round = 1
)

    $Units = @('B', 'KB', 'MB', 'GB')

    for ($i=0; $Bytes -ge 1024 -and $i -lt $Units.Length; $i++) {$Bytes = $Bytes / 1024}

    "$([Math]::Round($Bytes,$Round))$($Units[$i])"
}

## Format update size

function WUSize {
    
    param(
        
        [Parameter(Position = 0,Mandatory = $True)]
        [double]$MinSize,

        [Parameter(Position = 1,Mandatory = $True)]
        [double]$MaxSize
    )

    if ($MaxSize -eq $MinSize) {"($(WUUnits $MaxSize))"}
                
    else {"($(WUUnits $MinSize)-$(WUUnits $MaxSize))"}
}

## Update title and size for log message

function WUDetails {

    param(
    
        [Parameter(Position = 0,Mandatory = $True)]
        $Update
    )
    
    "$($Update.Title) $(WUSize $Update.MinDownloadSize $Update.MaxDownloadSize)"
}

## Send report email

function WUEmail {
        
    try{

        $Log = Import-Clixml -Path $LogPath

        if ($Log.Level -contains 'Error') {$Colour = 'Red' ; $Level = 'Error'}
        
        elseif ($Log.Level -contains 'Warning') {$Colour = 'Yellow' ; $Level = 'Warning'}
        
        else {$Colour = 'Green' ; $Level = 'Success'}

        $EmailHead = "<style>table {border-collapse: collapse;} table,th,td {border:1px solid black;font-size:14px;text-align:left;vertical-align:middle;padding: 5px;} th {background-color:$Colour;color:Black}</style>"

        $EmailBody = $Log | ConvertTo-Html -As Table -Head $EmailHead | Out-String

        $Message_Params = @{

            Subject = "zWindowsUpdate - $Level - $env:COMPUTERNAME"
            Body = $EmailBody
            BodyAsHtml = $true
            From = $SmtpFrom
            To = $SmtpTo
            SmtpServer = $SmtpServer
        }

        Send-MailMessage @Message_Params
    } 
        
    catch{WULog -Message "Email - $($_.Exception.Message)" -Level Error}

    finally {$Script:Sent = $true}
}

## Script Start

try {

    $CurrentErrorActionPreference = $ErrorActionPreference

    $ErrorActionPreference = 'Stop'

    $ResultCodes = @{0 = 'not started' ; 1 = 'in progress' ; 2 = 'succeeded' ; 3 = 'succeeded with errors' ; 4 = 'failed' ; 5 = 'aborted'}

    WULog -Message '## zWindowsUpdate ##' -Overwrite

    $PMessage = @()
    
    foreach ($P in ($PSBoundParameters.GetEnumerator()) | Sort-Object Key) {$PMessage += "Parameter - $($P.Key) = $($P.Value)"}
    
    if ($PMessage.Count -gt 0) {WULog -Message $PMessage}

    WULog -Message 'Starting windows update session'

    ## Create search criteria

    $SearchCriteria = "Type='Software' and IsInstalled=0 and IsHidden=0"

    if ($ExcludeOptional) {$SearchCriteria = $SearchCriteria + ' and BrowseOnly=0'}

    if ($AutoSelect) {$SearchCriteria = $SearchCriteria + ' and AutoSelectOnWebSites=1'}

    if ($UpdateType) {

        switch ($UpdateType) {

            {$UpdateType -contains 'Application'} {$TypeCriteria = $TypeCriteria + "($SearchCriteria and CategoryIDs contains '5C9376AB-8CE6-464A-B136-22113DD69801') or "}

            {$UpdateType -contains 'CriticalUpdates'} {$TypeCriteria = $TypeCriteria + "($SearchCriteria and CategoryIDs contains 'E6CF1350-C01B-414D-A61F-263D14D133B4') or "}

            {$UpdateType -contains 'Definitions'} {$TypeCriteria = $TypeCriteria + "($SearchCriteria and CategoryIDs contains 'E0789628-CE08-4437-BE74-2495B842F43B') or "}

            {$UpdateType -contains 'FeaturePacks'} {$TypeCriteria = $TypeCriteria + "($SearchCriteria and CategoryIDs contains 'B54E7D24-7ADD-428F-8B75-90A396FA584F') or "}

            {$UpdateType -contains 'SecurityUpdates'} {$TypeCriteria = $TypeCriteria + "($SearchCriteria and CategoryIDs contains '0FA1201D-4330-4FA8-8AE9-B877473B6441') or "}

            {$UpdateType -contains 'ServicePacks'} {$TypeCriteria = $TypeCriteria + "($SearchCriteria and CategoryIDs contains '68C5B0A3-D1A6-4553-AE49-01D3A7827828') or "}

            {$UpdateType -contains 'Tools'} {$TypeCriteria = $TypeCriteria + "($SearchCriteria and CategoryIDs contains 'B4832BD8-E735-4761-8DAF-37F882276DAB') or "}

            {$UpdateType -contains 'UpdateRollups'} {$TypeCriteria = $TypeCriteria + "($SearchCriteria and CategoryIDs contains '28BC880E-0592-4CBF-8F95-C79B17911D5F') or "}

            {$UpdateType -contains 'Updates'} {$TypeCriteria = $TypeCriteria + "($SearchCriteria and CategoryIDs contains 'CD5FFD1E-E932-4E3A-BF74-18BF0B1BBD83') or "}
        }

        $SearchCriteria = $TypeCriteria.TrimEnd(' or ')
    }

    ## Check for a pending reboot

    $PendingReboot = (New-Object -ComObject 'Microsoft.Update.SystemInfo').RebootRequired

    if (-not $PendingReboot) {

        ## Section 1 - Search

        ## Create update session object

        try {$UpdateSession = New-Object -ComObject 'Microsoft.Update.Session'}

        catch {throw "Update session - $($_.Exception.Message)"}

        ## Create update searcher

        try {$UpdateSearcher = $UpdateSession.CreateUpdateSearcher()}

        catch {throw "Update searcher - $($_.Exception.Message)"}

        ## Service selection

        if ($Service -eq 'WSUS') {$UpdateSearcher.ServerSelection = 1}

        elseif ($Service -eq 'WindowsUpdate') {$UpdateSearcher.ServerSelection = 2}

        elseif ($Service -eq 'MicrosoftUpdate') {

            try {
            
                $ServiceManager = $UpdateSession.CreateUpdateServiceManager()

                $MSService = $ServiceManager.Services | Where-Object {$_.name -eq 'Microsoft Update'}

                $UpdateSearcher.ServerSelection = 3

                if ($MSService) {$UpdateSearcher.ServiceID = $MSService.ServiceID}

                else {
                    
                    WULog -Message 'Attempting to add Microsoft Update service'

                    $MSServiceID = '7971f918-a847-4430-9279-4a52d1efe18d'
                    
                    $ServiceManager.AddService2($MSServiceID,2,'') | Out-Null

                    $UpdateSearcher.ServiceID = $MSServiceID
                }
            
            }

            catch {throw "Service - $($_.Exception.Message)"}        
        }

        ## Search for updates

        WULog -Message 'Searching for updates'
    
        try {$Updates = $UpdateSearcher.Search($SearchCriteria).Updates}

        catch {throw "Update searcher - $($_.Exception.Message)"}

        ## Filter updates by KBArticleIDs

        if ($IncludeKB) {

            $Updates = $Updates | ForEach-Object {

                foreach ($KB in $IncludeKB) {if ($KB -match $_.KBArticleIDs) {$_ ; break}}
            }
        }

        if ($ExcludeKB) {

            $Updates = $Updates | ForEach-Object {

                $IncludeUpdate = $true

                foreach ($KB in $ExcludeKB) {if ($KB -match $_.KBArticleIDs) {$IncludeUpdate = $false ; break}}

                if ($IncludeUpdate) {$_}
            }
        }

        $UpdateCount = ($Updates | Measure-Object).Count

        if ($UpdateCount -gt 0) {

            $UpdateMaxSize = ($Updates | Measure-Object -Sum -Property MaxDownloadSize).Sum

            $UpdateMinSize = ($Updates | Measure-Object -Sum -Property MinDownloadSize).Sum

            WULog -Message "Found $UpdateCount update(s) $(WUSize $UpdateMinSize $UpdateMaxSize)"
            
            $UMessage = $Updates | ForEach-Object {"$(WUDetails -Update $_)"}

            WULog -Message $UMessage

            if (-not $SearchOnly) {

                $UpdatesToDownload = @()

                ## Check if EULA needs accepted then either accept or don't download, depending on -AcceptEula parameter

                foreach ($Update in $Updates) {
     
                    if (($Update.EulaAccepted -eq $false)) {
                        
                        if ($AcceptEula) {
                            
                            try {
                                
                                WULog -Message "Accepting EULA - $(WUDetails -Update $Update)"

                                $Update.AcceptEula()

                                $UpdatesToDownload += $Update
                            }
                            
                            catch {WULog -Message "Accepting EULA failed, update will not be downloaded - $($_.Exception.Message) - $(WUDetails -Update $Update)" -Level Error}
                        }

                        else {WULog -Message "Update requires an EULA to be accepted and will not be downloaded - $(WUDetails -Update $Update)" -Level Warning}
                    }

                    else {$UpdatesToDownload += $Update}
                }

                $DownloadCount = ($UpdatesToDownload | Measure-Object).Count
        
                if ($DownloadCount -gt 0) {
                
                    ## Section 2 - Download

                    $UpdateMaxSize = ($UpdatesToDownload | Measure-Object -Sum -Property MaxDownloadSize).Sum

                    $UpdateMinSize = ($UpdatesToDownload | Measure-Object -Sum -Property MinDownloadSize).Sum

                    WULog -Message "Downloading $DownloadCount update(s) $(WUSize $UpdateMinSize $UpdateMaxSize)"

                    ## Create update downloader

                    try {$UpdateDownloader = $UpdateSession.CreateUpdateDownloader()} 
                
                    catch {throw "Update Downloader - $($_.Exception.Message)"}

                    $UpdatesToInstall = @()

                    $DownloadCounter = 1

                    ## Loop through updates to be downloaded

                    foreach ($Update in $UpdatesToDownload) {

                        ## Check if the update has already been downloaded

                        if ($Update.IsDownloaded -eq $true) {
                        
                            WULog -Message "Download $DownloadCounter of $DownloadCount already completed - $(WUDetails -Update $Update)"

                            $UpdatesToInstall += $Update
                        }

                        else {

                            try {

                                ## Download update

                                WULog -Message "Downloading update $DownloadCounter of $DownloadCount - $(WUDetails -Update $Update)"

                                $UpdateToDownload = New-object -ComObject 'Microsoft.Update.UpdateColl'
                    
                                $UpdateToDownload.Add($Update) | Out-Null
                    
                                $UpdateDownloader.Updates = $UpdateToDownload
                    
                                $DownloadResult = $UpdateDownloader.Download()

                                ## Check download result code

                                if ($DownloadResult.resultCode –eq 2) {
                    
                                    $UpdatesToInstall += $Update

                                    WULog -Message "Download $($ResultCodes[[int]$DownloadResult.ResultCode]) - $(WUDetails -Update $Update)"
                                }

                                elseif ($DownloadResult.resultCode –eq 3) {
                            
                                    $UpdatesToInstall += $Update

                                    WULog -Message "Download $($ResultCodes[[int]$DownloadResult.ResultCode]) - $(WUDetails -Update $Update)" -Level Warning
                                }
                    
                                else {WULog -Message "Download $($ResultCodes[[int]$DownloadResult.ResultCode]) - $(WUDetails -Update $Update)" -Level Error}
                            }

                            catch {WULog -Message "Update download - $($_.Exception.Message) - $(WUDetails -Update $Update)" -Level Error}
                        }

                        $DownloadCounter++
                    }

                    $InstallCount = ($UpdatesToInstall | Measure-Object).Count

                    if (-not $DownloadOnly) {

                        if ($InstallCount -gt 0) {

                            ## Section 3 - Install

                            $UpdateMaxSize = ($UpdatesToInstall | Measure-Object -Sum -Property MaxDownloadSize).Sum

                            $UpdateMinSize = ($UpdatesToInstall | Measure-Object -Sum -Property MinDownloadSize).Sum

                            WULog -Message "Installing $InstallCount update(s) $(WUSize $UpdateMinSize $UpdateMaxSize)"

                            ## Create update installer

                            try {$UpdateInstaller = $UpdateSession.CreateUpdateInstaller()}

                            catch {throw "Update installer - $($_.Exception.Message)"}

                            $InstallCounter = 1

                            $InstalledCount = 0

                            $RebootRequired = $false

                            ## Loop through updates to be installed

                            foreach ($Update in $UpdatesToInstall) {

                                WULog -Message "Installing update $InstallCounter of $InstallCount - $(WUDetails -Update $Update)"

                                try {

                                    ## Install update

                                    $UpdateToInstall = New-object -ComObject 'Microsoft.Update.UpdateColl'
        
                                    $UpdateToInstall.Add($Update) | Out-Null

                                    $UpdateInstaller.Updates = $UpdateToInstall
                        
                                    $InstallResult = $UpdateInstaller.Install()

                                    ## Check install result code

                                    if ($InstallResult.ResultCode –eq 2) {

                                        WULog -Message "Install $($ResultCodes[[int]$InstallResult.ResultCode]) - $(WUDetails -Update $Update)"

                                        if ($InstallResult.RebootRequired) {$RebootRequired = $true}

                                        $InstalledCount++
                                    }

                                    elseif ($InstallResult.resultCode –eq 3) {
                                    
                                        WULog -Message "Install $($ResultCodes[[int]$InstallResult.ResultCode]) - $(WUDetails -Update $Update)" -Level Warning

                                        if ($InstallResult.RebootRequired) {$RebootRequired = $true}

                                        $InstalledCount++
                                    }
                    
                                    else {WULog -Message "Install $($ResultCodes[[int]$InstallResult.ResultCode]) - $(WUDetails -Update $Update)" -Level Error}
                                }

                                catch {WULog -Message "Update Install - $($_.Exception.Message) - $(WUDetails -Update $Update)" -Level Error}

                                $InstallCounter++
                            }

                            if ($RebootRequired) {

                                if ($Reboot) {
                            
                                    WULog -Message "Finished - Attempting to reboot computer - $InstalledCount of $UpdateCount update(s) installed"

                                    if ($SendEmail) {WUEmail}

                                    try {Restart-Computer -Force}
                            
                                    catch {WULog -Message "Reboot failed - $($_.Exception.Message)" -Level Error}
                                }

                                else {WULog -Message "Finished - Reboot required - $InstalledCount of $UpdateCount update(s) installed" -Level Warning}
                            }

                            else {WULog -Message "Finished - No reboot required - $InstalledCount of $UpdateCount update(s) installed"}
                        }

                        else {WULog -Message 'Finished - No updates to install'}
                    }

                    else {WULog -Message "Finished - Download only - $InstallCount of $UpdateCount update(s) downloaded"}
                }

                else {WULog -Message 'Finished - No updates to download'}
            }
        
            else {WULog -Message "Finished - Search only - $UpdateCount update(s) found"}
        }

        else {WULog -Message 'Finished - No updates found'}
    }

    else {
    
        if ($Reboot) {
                            
            WULog -Message 'Finished - Attempting to reboot computer'

            if ($SendEmail) {WUEmail}

            try {Restart-Computer -Force}
                            
            catch {WULog -Message "Reboot failed - $($_.Exception.Message)" -Level Error}
        }

        else {WULog -Message 'Finished - Pending reboot' -Level Warning}
    }
}

catch {WULog -Message "Finished - $($_.Exception.Message)" -Level Error}

finally {

    if ($SendEmail -and -not $Sent) {WUEmail}

    $ErrorActionPreference = $CurrentErrorActionPreference
}