public/Install-KbUpdate.ps1

# requires 5
function Install-KbUpdate {
    <#
    .SYNOPSIS
        Installs KBs on local and remote servers on Windows-based systems
 
    .DESCRIPTION
        Installs KBs on local and remote servers on Windows-based systems
 
        PowerShell 5.1 must be installed and enabled on the target machine and the target machine must be Windows-based
 
        Note that if you use a DSC Pull server, this may impact your LCM
 
    .PARAMETER ComputerName
        Used to connect to a remote host
 
    .PARAMETER Credential
        The optional alternative credential to be used when connecting to ComputerName
 
    .PARAMETER PSDscRunAsCredential
        Run the install as a specific user (other than SYSTEM) on the target node
 
    .PARAMETER HotfixId
        The HotfixId of the patch
 
    .PARAMETER FilePath
        The filepath of the patch. Not required - if you don't have it, we can grab it from the internet
 
        Note this does place the hotfix files in your local and remote Downloads directories
 
    .PARAMETER Guid
        If the file is an exe and no GUID is specified, we will have to get it from Get-KbUpdate
 
    .PARAMETER Title
        If the file is an exe and no Title is specified, we will have to get it from Get-KbUpdate
 
    .PARAMETER Method
        Used to specify DSC or WindowsUpdate. By default, WindowsUpdate is used on localhost and DSC is used on remote servers.
 
    .PARAMETER ArgumentList
        This is an advanced parameter for those of you who need special argumentlists for your platform-specific update.
 
        The argument list required by SQL updates are already accounted for.
 
    .PARAMETER InputObject
        Allows infos to be piped in from Get-KbUpdate
 
    .PARAMETER EnableException
        By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message.
        This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting.
        Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch.
 
    .NOTES
        Author: Chrissy LeMaire (@cl), Jess Pomfret (@jpomfret)
        Copyright: (c) licensed under MIT
        License: MIT https://opensource.org/licenses/MIT
 
    .EXAMPLE
        PS C:\> Install-KbUpdate -ComputerName sql2017 -FilePath C:\temp\windows10.0-kb4534273-x64_74bf76bc5a941bbbd0052caf5c3f956867e1de38.msu
 
        Installs KB4534273 from the C:\temp directory on sql2017
 
    .EXAMPLE
        PS C:\> Install-KbUpdate -ComputerName sql2017 -FilePath \\dc\sql\windows10.0-kb4532947-x64_20103b70445e230e5994dc2a89dc639cd5756a66.msu
 
        Installs KB4534273 from the \\dc\sql\ directory on sql2017
 
    .EXAMPLE
        PS C:\> Install-KbUpdate -ComputerName sql2017 -HotfixId kb4486129
 
        Downloads an update, stores it in Downloads and installs it from there
 
    .EXAMPLE
        PS C:\> $params = @{
            ComputerName = "sql2017"
            FilePath = "C:\temp\sqlserver2017-kb4498951-x64_b143d28a48204eb6ebab62394ce45df53d73f286.exe"
            Verbose = $true
        }
        PS C:\> Install-KbUpdate @params
        PS C:\> Uninstall-KbUpdate -ComputerName sql2017 -HotfixId KB4498951
 
        Installs KB4498951 on sql2017 then uninstalls it ✔
 
    .EXAMPLE
        PS C:\> Get-KbNeededUpdate -OutVariable needed | Save-KbUpdate -Path C:\temp
        PS C:\> $needed | Install-KbUpdate -RepositoryPath C:\temp
 
        Saves the files for needed updates then installs them from that path
 
    .EXAMPLE
        PS C:\> Get-KbNeededUpdate | Install-KbUpdate -Method WindowsUpdate -Verbose
 
        Installs needed updates, only works on localhost
 
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = "Medium")]
    param (
        [PSFComputer[]]$ComputerName = $env:ComputerName,
        [PSCredential]$Credential,
        [PSCredential]$PSDscRunAsCredential,
        [Parameter(ValueFromPipelineByPropertyName)]
        [Alias("Name", "KBUpdate", "Id")]
        [string]$HotfixId,
        [Alias("Path")]
        [string]$FilePath,
        [string]$RepositoryPath,
        [ValidateSet("WindowsUpdate", "DSC")]
        [string]$Method,
        [Parameter(ValueFromPipelineByPropertyName)]
        [Alias("UpdateId")]
        [string]$Guid,
        [Parameter(ValueFromPipelineByPropertyName)]
        [string]$Title,
        [string]$ArgumentList,
        [Parameter(ValueFromPipeline)]
        [pscustomobject[]]$InputObject,
        [switch]$EnableException
    )
    process {
        if (-not $PSBoundParameters.HotfixId -and -not $PSBoundParameters.FilePath -and -not $PSBoundParameters.InputObject) {
            Stop-PSFFunction -EnableException:$EnableException -Message "You must specify either HotfixId or FilePath or pipe in the results from Get-KbUpdate"
            return
        }

        if ($IsLinux -or $IsMacOs) {
            Stop-PSFFunction -Message "This command using remoting and only supports Windows at this time" -EnableException:$EnableException
            return
        }

        if (-not $HotfixId.ToUpper().StartsWith("KB") -and $PSBoundParameters.HotfixId) {
            $HotfixId = "KB$HotfixId"
        }

        if ($Credential.UserName) {
            $PSDefaultParameterValues["*:Credential"] = $Credential
        }

        if (-not $PSBoundParameters.ComputerName -and $InputObject) {
            $ComputerName = [PSFComputer]$InputObject.ComputerName
            Write-PSFMessage -Level Verbose -Message "Added $ComputerName"
        }

        foreach ($item in $ComputerName) {
            $computer = $item.ComputerName
            $completed++

            if ($item.IsLocalHost -and -not (Test-ElevationRequirement -ComputerName $computer)) {
                Stop-PSFFunction -EnableException:$EnableException -Message "You must be an administrator to run this command on the local host" -Continue
            }

            if (-not $item.IsLocalHost -and $Method -eq "WindowsUpdate") {
                Stop-PSFFunction -EnableException:$EnableException -Message "The Windows Update method is only supported on localhost due to Windows security restrictions" -Continue
            }

            if ((Get-Service wuauserv | Where-Object StartType -ne Disabled) -and $Method -eq "WindowsUpdate") {
                Stop-PSFFunction -EnableException:$EnableException -Message "The Windows Update method cannot be used when the Windows Update service is stopped on $computer" -Continue
            }

            # null out a couple things to be safe
            $remotefileexists = $programhome = $remotesession = $null
            Write-PSFMessage -Level Verbose -Message "Processing $computer"
            if ($item.IsLocalhost -and $Method -ne "DSC") {
                if ((Get-Service wuauserv | Where-Object StartType -ne Disabled)) {
                    $dowin = $true
                } else {
                    $dowin = $false
                }
            }

            if ($dowin -or $Method -eq "WindowsUpdate") {
                try {
                    Write-PSFMessage -Level Verbose -Message "Using the Windows Update method"
                    $sessiontype = [type]::GetTypeFromProgID("Microsoft.Update.Session")
                    $session = [activator]::CreateInstance($sessiontype)
                    $session.ClientApplicationID = "kbupdate installer"

                    if ($InputObject.UpdateId) {
                        Write-PSFMessage -Level Verbose -Message "Got an UpdateId"
                        $searchresult = $session.CreateUpdateSearcher().Search("UpdateId = '$($InputObject.UpdateId)'")
                    } else {
                        Write-PSFMessage -Level Verbose -Message "Build needed updates"
                        $searchresult = $session.CreateUpdateSearcher().Search("Type='Software' and IsInstalled=0 and IsHidden=0")
                    }
                } catch {
                    Stop-PSFFunction -EnableException:$EnableException -Message "Failed to create update searcher" -ErrorRecord $_ -Continue
                }

                # iterate the updates in searchresult
                # it must be force iterated like this
                if ($searchresult.Updates) {
                    Write-PSFMessage -Level Verbose -Message "Processing $($searchresult.Updates.Count) updates"
                    foreach ($update in $searchresult.Updates) {
                        $updateinstall = New-Object -ComObject 'Microsoft.Update.UpdateColl'
                        Write-PSFMessage -Level Verbose -Message "Accepting EULA for $($update.Title)"
                        $null = $update.AcceptEula()
                        foreach ($bundle in $update.BundledUpdates) {
                            $files = New-Object -ComObject "Microsoft.Update.StringColl.1"
                            foreach ($file in $bundle.DownloadContents.DownloadUrl) {
                                if ($RepositoryPath) {
                                    $filename = Split-Path -Path $file.DownloadUrl -Leaf
                                    Write-PSFMessage -Level Verbose -Message "Adding $filename"
                                    $fullpath = Join-Path -Path $RepositoryPath -ChildPath $filename
                                    Write-PSFMessage -Level Verbose -Message $fullpath
                                    $null = $files.Add($fullpath)
                                }
                            }
                        }
                        Write-PSFMessage -Level Verbose -Message "Checking to see if IsDownloaded ($($update.IsDownloaded)) is true"
                        if ($update.IsDownloaded) {
                            Write-PSFMessage -Level Verbose -Message "Updates for $($update.Title) do not need to be downloaded"
                        } else {
                            Write-PSFMessage -Level Verbose -Message "Update for $($update.Title) needs to be downloaded"
                            try {
                                Write-PSFMessage -Level Verbose -Message "Creating update downloader"
                                $downloader = $session.CreateUpdateDownloader()
                                Write-PSFMessage -Level Verbose -Message "Adding Updates"
                                $downloader.Updates = $searchresult.Updates
                                Write-PSFMessage -Level Verbose -Message "Executing download"
                                $null = $downloader.Download()
                            } catch {
                                Stop-PSFFunction -EnableException:$EnableException -Message "Failure on $env:ComputerName" -ErrorRecord $PSItem -Continue
                            }
                        }
                        $updateinstall.Add($update) | Out-Null

                        try {
                            Write-PSFMessage -Level Verbose -Message "Creating installer object for $($update.Title)"
                            $installer = $session.CreateUpdateInstaller()
                            if ($updateinstall) {
                                Write-PSFMessage -Level Verbose -Message "Adding updates via updateinstall"
                                $installer.Updates = $updateinstall
                            } else {
                                Write-PSFMessage -Level Verbose -Message "Adding updates via .Updates"
                                $installer.Updates = $searchresult.Updates
                            }

                            Write-PSFMessage -Level Verbose -Message "Installing updates"

                            Write-ProgressHelper -Activity "Installing updates on $computer" -Message "Installing $($update.Title)" -ExcludePercent
                            $results = $installer.Install()

                            if ($results.RebootRequired -and $HResult -eq 0) {
                                $status = "Success - Reboot required"
                            } else {
                                switch ($results.ResultCode) {
                                    1 {
                                        $status = "In Progress"
                                    }
                                    2 {
                                        $status = "Succeeded"
                                    }
                                    3 {
                                        $status = "Succeeded with errors"
                                    }
                                    4 {
                                        $status = "Failed"
                                    }
                                    5 {
                                        $status = "Aborted"
                                    }
                                    default {
                                        $status = "Failure"
                                    }
                                }
                            }
                            if ($update.BundledUpdates.DownloadContents.DownloadUrl) {
                                $filename = Split-Path -Path $update.BundledUpdates.DownloadContents.DownloadUrl -Leaf
                            } else {
                                $filename = $null
                            }

                            [pscustomobject]@{
                                ComputerName = $computer
                                Title        = $update.Title
                                ID           = $update.Identity.UpdateID
                                Status       = $status
                                HotFixId     = ($update.KBArticleIDs | Select-Object -First 1)
                                Update       = $update
                            } | Select-DefaultView -Property ComputerName, Title, HotFixId, Id, Status
                        } catch {
                            Stop-PSFFunction -EnableException:$EnableException -Message "Failure on $env:ComputerName" -ErrorRecord $PSItem -Continue
                        }
                    }
                } else {
                    $files = New-Object -ComObject "Microsoft.Update.StringColl.1"
                    $updateinstall = New-Object -ComObject 'Microsoft.Update.UpdateColl'

                    Write-PSFMessage -Level Verbose -Message "Link"
                    foreach ($link in $searchresult.Link) {
                        if ($link -and $RepositoryPath) {
                            $filename = Split-Path -Path $link -Leaf
                            $fullpath = Join-Path -Path $RepositoryPath -ChildPath $filename
                            Write-PSFMessage -Level Verbose -Message "Adding $fullpath"
                            $null = $files.Add($fullpath)
                        }
                    }

                    # load into Windows Update API
                    try {
                        Write-PSFMessage -Level Verbose -Message "Copying files to cache"
                        $bundle.CopyToCache($files)
                    } catch {
                        Stop-PSFFunction -EnableException:$EnableException -Message "Failure on $env:ComputerName" -ErrorRecord $PSItem -Continue
                    }

                    try {
                        Write-PSFMessage -Level Verbose -Message "Creating installer object"
                        $installer = $session.CreateUpdateInstaller()
                        if ($updateinstall) {
                            Write-PSFMessage -Level Verbose -Message "Adding updates via updateinstall"
                            $installer.Updates = $updateinstall
                        } else {
                            Write-PSFMessage -Level Verbose -Message "Adding updates via .Updates"
                            $installer.Updates = $searchresult.Updates
                        }

                        Write-PSFMessage -Level Verbose -Message "Installing updates"

                        Write-ProgressHelper -Activity "Installing updates on $computer" -Message "Installing $($update.Title)" -ExcludePercent
                        $results = $installer.Install()

                        if ($results.RebootRequired -and $HResult -eq 0) {
                            $status = "Success - Reboot required"
                        } else {
                            switch ($results.ResultCode) {
                                1 {
                                    $status = "In Progress"
                                }
                                2 {
                                    $status = "Succeeded"
                                }
                                3 {
                                    $status = "Succeeded with errors"
                                }
                                4 {
                                    $status = "Failed"
                                }
                                5 {
                                    $status = "Aborted"
                                }
                                default {
                                    $status = "Failure"
                                }
                            }
                            if ($update.BundledUpdates.DownloadContents.DownloadUrl) {
                                $filename = Split-Path -Path $update.BundledUpdates.DownloadContents.DownloadUrl -Leaf
                            } else {
                                $filename = $null
                            }

                            [pscustomobject]@{
                                ComputerName = $computer
                                Title        = $update.Title
                                ID           = $update.Identity.UpdateID
                                Status       = $status
                                HotFixId     = ($update.KBArticleIDs | Select-Object -First 1)
                                Update       = $update
                            } | Select-DefaultView -Property ComputerName, Title, HotFixId, Id, Status
                        }
                    } catch {
                        Stop-PSFFunction -EnableException:$EnableException -Message "Failure on $env:ComputerName" -ErrorRecord $PSItem -Continue
                    }
                }
            } else {
                # Method is DSC
                if ($PSDefaultParameterValues["Invoke-PSFCommand:ComputerName"]) {
                    $null = $PSDefaultParameterValues.Remove("Invoke-PSFCommand:ComputerName")
                }

                if ($item.IsLocalHost) {
                    # a lot of the file copy work will be done in the $home dir
                    $programhome = Invoke-PSFCommand -ScriptBlock { $home }
                } else {
                    Write-PSFMessage -Level Verbose -Message "Adding $computer to PSDefaultParameterValues for Invoke-PSFCommand:ComputerName"
                    $PSDefaultParameterValues["Invoke-PSFCommand:ComputerName"] = $computer

                    Write-PSFMessage -Level Verbose -Message "Initializing remote session to $computer and also getting the remote home directory"
                    $programhome = Invoke-PSFCommand -ScriptBlock { $home }

                    if (-not $remotesession) {
                        $remotesession = Get-PSSession -ComputerName $computer -Verbose | Where-Object { $PsItem.Availability -eq 'Available' -and ($PsItem.Name -match 'WinRM' -or $PsItem.Name -match 'Runspace') } | Select-Object -First 1
                    }

                    if (-not $remotesession) {
                        $remotesession = Get-PSSession -ComputerName $computer | Where-Object { $PsItem.Availability -eq 'Available' } | Select-Object -First 1
                    }

                    if (-not $remotesession) {
                        Stop-PSFFunction -EnableException:$EnableException -Message "Session for $computer can't be found or no runspaces are available. Please file an issue on the GitHub repo at https://github.com/potatoqualitee/kbupdate/issues" -Continue
                    }
                }

                # fix for SYSTEM which doesn't have a downloads directory by default
                Write-PSFMessage -Level Verbose -Message "Checking for home downloads directory"
                Invoke-PSFCommand -ScriptBlock {
                    if (-not (Test-Path -Path "$home\Downloads")) {
                        Write-Warning "Creating Downloads directory at $home\Downloads"
                        $null = New-Item -ItemType Directory -Force -Path "$home\Downloads"
                    }
                }

                $hasxhotfix = Invoke-PSFCommand -ScriptBlock {
                    Get-Module -ListAvailable xWindowsUpdate -ErrorAction Ignore | Where-Object Version -eq 3.0.0
                }

                if (-not $hasxhotfix) {
                    try {
                        # Copy xWindowsUpdate to Program Files. The module is pretty much required to be in the PS Modules directory.
                        $oldpref = $ProgressPreference
                        $ProgressPreference = "SilentlyContinue"
                        $programfiles = Invoke-PSFCommand -ScriptBlock {
                            $env:ProgramFiles
                        }
                        if ($item.IsLocalhost) {
                            Write-PSFMessage -Level Verbose -Message "Copying xWindowsUpdate to $computer (local to $programfiles\WindowsPowerShell\Modules\xWindowsUpdate)"
                            $null = Copy-Item -Path "$script:ModuleRoot\library\xWindowsUpdate" -Destination "$programfiles\WindowsPowerShell\Modules" -Recurse -Force
                        } else {
                            Write-PSFMessage -Level Verbose -Message "Copying xWindowsUpdate to $computer (remote to $programfiles\WindowsPowerShell\Modules\xWindowsUpdate)"
                            $null = Copy-Item -Path "$script:ModuleRoot\library\xWindowsUpdate" -Destination "$programfiles\WindowsPowerShell\Modules" -ToSession $remotesession -Recurse -Force
                        }

                        $ProgressPreference = $oldpref
                    } catch {
                        Stop-PSFFunction -EnableException:$EnableException -Message "Couldn't auto-install xHotfix on $computer. Please Install-Module xWindowsUpdate on $computer to continue." -Continue
                    }
                }

                $hasxdsc = Invoke-PSFCommand -ScriptBlock {
                    Get-Module -ListAvailable xPSDesiredStateConfiguration -ErrorAction Ignore | Where-Object Version -eq 9.2.0
                }

                if (-not $hasxdsc) {
                    try {
                        Write-PSFMessage -Level Verbose -Message "Adding xPSDesiredStateConfiguration to $computer"
                        # Copy xWindowsUpdate to Program Files. The module is pretty much required to be in the PS Modules directory.
                        $oldpref = $ProgressPreference
                        $ProgressPreference = "SilentlyContinue"
                        $programfiles = Invoke-PSFCommand -ScriptBlock {
                            $env:ProgramFiles
                        }
                        if ($item.IsLocalhost) {
                            Write-PSFMessage -Level Verbose -Message "Copying xPSDesiredStateConfiguration to $computer (local to $programfiles\WindowsPowerShell\Modules\xPSDesiredStateConfiguration)"
                            $null = Copy-Item -Path "$script:ModuleRoot\library\xPSDesiredStateConfiguration" -Destination "$programfiles\WindowsPowerShell\Modules" -Recurse -Force
                        } else {
                            Write-PSFMessage -Level Verbose -Message "Copying xPSDesiredStateConfiguration to $computer (remote)"
                            $null = Copy-Item -Path "$script:ModuleRoot\library\xPSDesiredStateConfiguration" -Destination "$programfiles\WindowsPowerShell\Modules" -ToSession $remotesession -Recurse -Force
                        }

                        $ProgressPreference = $oldpref
                    } catch {
                        Stop-PSFFunction -EnableException:$EnableException -Message "Couldn't auto-install newer DSC resources on $computer. Please Install-Module xPSDesiredStateConfiguration version 9.2.0 on $computer to continue." -Continue
                    }
                }

                if ($InputObject.Link -and $RepositoryPath) {
                    $filename = Split-Path -Path $InputObject.Link -Leaf
                    Write-PSFMessage -Level Verbose -Message "Adding $filename"
                    $FilePath = Join-Path -Path $RepositoryPath -ChildPath $filename
                    $PSBoundParameters.FilePath = Join-Path -Path $RepositoryPath -ChildPath $filename
                    Write-PSFMessage -Level Verbose -Message "Adding $($PSBoundParameters.FilePath)"
                }

                if ($PSBoundParameters.FilePath) {
                    $remotefileexists = $updatefile = Invoke-PSFCommand -ArgumentList $FilePath -ScriptBlock {
                        Get-ChildItem -Path $args -ErrorAction SilentlyContinue
                    }
                }

                if (-not $remotefileexists) {
                    if ($FilePath) {
                        # try really hard to find it locally
                        $updatefile = Get-ChildItem -Path $FilePath -ErrorAction SilentlyContinue
                        if (-not $updatefile) {
                            Write-PSFMessage -Level Verbose -Message "Update file not found, try in Downloads"
                            $filename = Split-Path -Path $FilePath -Leaf
                            $updatefile = Get-ChildItem -Path "$home\Downloads\$filename" -ErrorAction SilentlyContinue
                        }
                    }

                    if (-not $updatefile) {
                        Write-PSFMessage -Level Verbose -Message "Update file not found, download it for them"
                        # try to automatically download it for them
                        if (-not $PSBoundParameters.InputObject) {
                            $InputObject = Get-KbUpdate -Architecture x64 -Latest -Pattern $HotfixId | Where-Object Link
                        }

                        # note to reader: if this picks the wrong one, please download the required file manually.
                        if ($InputObject.Link) {
                            if ($InputObject.Link -match 'x64') {
                                $file = $InputObject | Where-Object Link -match 'x64' | Select-Object -ExpandProperty Link -Last 1 | Split-Path -Leaf
                            } else {
                                $file = Split-Path $InputObject.Link -Leaf | Select-Object -Last 1
                            }
                        } else {
                            Stop-PSFFunction -EnableException:$EnableException -Message "Could not find file on $computer and couldn't find it online. Try piping in exactly what you'd like from Get-KbUpdate." -Continue
                        }

                        if ((Test-Path -Path "$home\Downloads\$file")) {
                            $updatefile = Get-ChildItem -Path "$home\Downloads\$file"
                        } else {
                            if ($PSCmdlet.ShouldProcess($computer, "File not detected, downloading now to $home\Downloads and copying to remote computer")) {
                                $warnatbottom = $true

                                # fix for SYSTEM which doesn't have a downloads directory by default
                                Write-PSFMessage -Level Verbose -Message "Checking for home downloads directory"
                                if (-not (Test-Path -Path "$home\Downloads")) {
                                    Write-PSFMessage -Level Warning -Message "Creating Downloads directory at $home\Downloads"
                                    $null = New-Item -ItemType Directory -Force -Path "$home\Downloads"
                                }

                                $updatefile = $InputObject | Select-Object -First 1 | Save-KbUpdate -Path "$home\Downloads"
                            }
                        }
                    }

                    if (-not $PSBoundParameters.FilePath) {
                        $FilePath = "$programhome\Downloads\$(Split-Path -Leaf $updateFile)"
                    }

                    if ($item.IsLocalhost) {
                        $remotefile = $updatefile
                    } else {
                        $remotefile = "$programhome\Downloads\$(Split-Path -Leaf $updateFile)"
                    }

                    # copy over to destination server unless
                    # it's local or it's on a network share
                    if (-not "$($PSBoundParameters.FilePath)".StartsWith("\\") -and -not $item.IsLocalhost) {
                        Write-PSFMessage -Level Verbose -Message "Update is not located on a file server and not local, copying over the remote server"
                        try {
                            $exists = Invoke-PSFCommand -ComputerName $computer -ArgumentList $remotefile -ScriptBlock {
                                Get-ChildItem -Path $args -ErrorAction SilentlyContinue
                            }
                            if (-not $exists) {
                                $null = Copy-Item -Path $updatefile -Destination $remotefile -ToSession $remotesession -ErrorAction Stop
                                $deleteremotefile = $remotefile
                            }
                        } catch {
                            $null = Invoke-PSFCommand -ComputerName $computer -ArgumentList $remotefile -ScriptBlock {
                                Remove-Item $args -Force -ErrorAction SilentlyContinue
                            }
                            try {
                                Write-PSFMessage -Level Warning -Message "Copy failed, trying again"
                                $null = Copy-Item -Path $updatefile -Destination $remotefile -ToSession $remotesession -ErrorAction Stop
                                $deleteremotefile = $remotefile
                            } catch {
                                $null = Invoke-PSFCommand -ComputerName $computer -ArgumentList $remotefile -ScriptBlock {
                                    Remove-Item $args -Force -ErrorAction SilentlyContinue
                                }
                                Stop-PSFFunction -EnableException:$EnableException -Message "Could not copy $updatefile to $remotefile" -ErrorRecord $PSItem -Continue
                            }
                        }
                    }
                }

                # if user doesnt add kb, try to find it for them from the provided filename
                if (-not $PSBoundParameters.HotfixId) {
                    $HotfixId = $FilePath.ToUpper() -split "\-" | Where-Object { $psitem.Startswith("KB") }
                }

                # i probably need to fix some logic but until then, check a few things
                if ($item.IsLocalHost) {
                    if ($updatefile) {
                        $FilePath = $updatefile
                    } else {
                        $updatefile = Get-ChildItem -Path $FilePath
                    }
                    if (-not $PSBoundParameters.Title) {
                        Write-PSFMessage -Level Verbose -Message "Trying to get Title from $($updatefile.FullName)"
                        $Title = $updatefile.VersionInfo.ProductName
                    }
                } elseif ($remotefile) {
                    $FilePath = $remotefile
                }

                if ($FilePath.EndsWith("exe")) {
                    if (-not $PSBoundParameters.ArgumentList -and $FilePath -match "sql") {
                        $ArgumentList = "/action=patch /AllInstances /quiet /IAcceptSQLServerLicenseTerms"
                    } else {
                        # Setting a default argumentlist that hopefully works for most things?
                        $ArgumentList = "/install /quiet /notrestart"
                    }

                    if (-not $Guid) {
                        if ($InputObject) {
                            $Guid = $PSBoundParameters.InputObject.Guid
                            $Title = $PSBoundParameters.InputObject.Title
                        } else {
                            if ($true) {
                                try {
                                    $hotfixid = $guid = $null
                                    Write-PSFMessage -Level Verbose -Message "Trying to get Title from $($updatefile.FullName)"
                                    $updatefile = Get-ChildItem -Path $updatefile.FullName -ErrorAction SilentlyContinue
                                    $Title = $updatefile.VersionInfo.ProductName
                                    Write-PSFMessage -Level Verbose -Message "Trying to get GUID from $($updatefile.FullName)"

                                    <#
                                    The reason you want to find the GUID is to save time, mostly, I guess?
 
                                    It saves time because it won't even attempt the install if there are GUID matches
                                    in the registry. If you pass a fake but compliant GUID, it attempts the install and
                                    fails, no big deal.
 
                                    Overall, it just seems like a good idea to get a GUID if it's required.
                                #>


                                    <#
                                    It's better to just read from memory but I can't get this to work
                                    $cab = New-Object Microsoft.Deployment.Compression.Cab.Cabinfo "C:\path\path.exe"
                                    $file = New-Object Microsoft.Deployment.Compression.Cab.CabFileInfo($cab, "0")
                                    $content = $file.OpenRead()
                                #>


                                    $cab = New-Object Microsoft.Deployment.Compression.Cab.Cabinfo $updatefile.FullName
                                    $files = $cab.GetFiles("*")
                                    $index = $files | Where-Object Name -eq 0
                                    if (-not $index) {
                                        $index = $files | Where-Object Name -match "none.xml| ParameterInfo.xml"
                                    }
                                    $temp = Get-PSFPath -Name Temp
                                    $indexfilename = $index.Name
                                    $xmlfile = Join-Path -Path $temp -ChildPath "$($updatefile.BaseName).xml"
                                    $null = $cab.UnpackFile($indexfilename, $xmlfile)
                                    if ((Test-Path -Path $xmlfile)) {
                                        $xml = [xml](Get-Content -Path $xmlfile)
                                        $tempguid = $xml.BurnManifest.Registration.Id
                                    }

                                    if (-not $tempguid -and $xml.MsiPatch.PatchGUID) {
                                        $tempguid = $xml.MsiPatch.PatchGUID
                                    }
                                    if (-not $tempguid -and $xml.Setup.Items.Patches.MSP.PatchCode) {
                                        $tempguid = $xml.Setup.Items.Patches.MSP.PatchCode
                                    }

                                    Get-ChildItem -Path $xmlfile -ErrorAction SilentlyContinue | Remove-Item -Confirm:$false -ErrorAction SilentlyContinue

                                    # if we can't find the guid, use one that we know
                                    # is valid but not associated with any hotfix
                                    if (-not $tempguid) {
                                        $tempguid = "DAADB00F-DAAD-B00F-B00F-DAADB00FB00F"
                                    }

                                    $guid = ([guid]$tempguid).Guid
                                } catch {
                                    $guid = "DAADB00F-DAAD-B00F-B00F-DAADB00FB00F"
                                }

                                Write-PSFMessage -Level Verbose -Message "GUID is $guid"
                            }
                        }
                    }

                    # this takes care of things like SQL Server updates
                    $hotfix = @{
                        Name       = 'xPackage'
                        ModuleName = @{
                            ModuleName    = "xPSDesiredStateConfiguration"
                            ModuleVersion = "9.2.0"
                        }
                        Property   = @{
                            Ensure     = 'Present'
                            ProductId  = $Guid
                            Name       = $Title
                            Path       = $FilePath
                            Arguments  = $ArgumentList
                            ReturnCode = 0, 3010
                        }
                    }
                } else {
                    # this takes care of WSU files
                    $hotfix = @{
                        Name       = 'xHotFix'
                        ModuleName = @{
                            ModuleName    = "xWindowsUpdate"
                            ModuleVersion = "3.0.0"
                        }
                        Property   = @{
                            Ensure = 'Present'
                            Id     = $HotfixId
                            Path   = $FilePath
                        }
                    }
                    if ($PSDscRunAsCredential) {
                        $hotfix.Property.PSDscRunAsCredential = $PSDscRunAsCredential
                    }
                }

                if ($PSCmdlet.ShouldProcess($computer, "Installing file from $FilePath")) {
                    try {
                        $null = Invoke-PSFCommand -ScriptBlock {
                            param (
                                $Hotfix,
                                $VerbosePreference,
                                $ManualFileName
                            )
                            Import-Module xPSDesiredStateConfiguration -RequiredVersion 9.2.0 -Force
                            Import-Module xWindowsUpdate -RequiredVersion 3.0.0 -Force
                            $PSDefaultParameterValues.Remove("Invoke-WebRequest:ErrorAction")
                            $PSDefaultParameterValues['*:ErrorAction'] = 'SilentlyContinue'
                            $ErrorActionPreference = "Stop"

                            if (-not (Get-Command Invoke-DscResource)) {
                                throw "Invoke-DscResource not found on $env:ComputerName"
                            }
                            $null = Import-Module xWindowsUpdate -Force
                            Write-Verbose -Message "Installing $($hotfix.property.id) from $($hotfix.property.path)"
                            try {
                                if (-not (Invoke-DscResource @hotfix -Method Test)) {
                                    Invoke-DscResource @hotfix -Method Set -ErrorAction Stop
                                }
                            } catch {
                                $message = "$_"

                                # Unsure how to figure out params, try another way
                                if ($message -match "The return code 1 was not expected.") {
                                    try {
                                        Write-Verbose -Message "Retrying install with /quit parameter"
                                        $hotfix.Property.Arguments = "/quiet"
                                        Invoke-DscResource @hotfix -Method Set -ErrorAction Stop
                                    } catch {
                                        $message = "$_"
                                    }
                                }

                                switch ($message) {
                                    # some things can be ignored
                                    { $message -match "Serialized XML is nested too deeply" -or $message -match "Name does not match package details" } {
                                        $null = 1
                                    }
                                    { $message -match "2359302" } {
                                        throw "Error 2359302: update is already installed on $env:ComputerName"
                                    }
                                    { $message -match "could not be started" } {
                                        throw "The install coult not initiate. The $($hotfix.Property.Path) on $env:ComputerName may be corrupt or only partially downloaded. Delete it and try again."
                                    }
                                    { $message -match "2042429437" } {
                                        throw "Error -2042429437. Configuration is likely not correct. The requested features may not be installed or features are already at a higher patch level."
                                    }
                                    { $message -match "2068709375" } {
                                        throw "Error -2068709375. The exit code suggests that something is corrupt. See if this tutorial helps: http://www.sqlcoffee.com/Tips0026.htm"
                                    }
                                    { $message -match "2067919934" } {
                                        throw "Error -2067919934 You likely need to reboot $env:ComputerName."
                                    }
                                    { $message -match "2147942402" } {
                                        throw "System can't find the file specified for some reason."
                                    }
                                    default {
                                        throw
                                    }
                                }
                            }
                        } -ArgumentList $hotfix, $VerbosePreference, $PSBoundParameters.FileName -ErrorAction Stop

                        if ($deleteremotefile) {
                            Write-PSFMessage -Level Verbose -Message "Deleting $deleteremotefile"
                            $null = Invoke-PSFCommand -ComputerName $computer -ArgumentList $deleteremotefile -ScriptBlock {
                                Get-ChildItem -ErrorAction SilentlyContinue $args | Remove-Item -Force -ErrorAction SilentlyContinue -Confirm:$false
                            }
                        }

                        Write-Verbose -Message "Finished installing, checking status"
                        $exists = Get-KbInstalledUpdate -ComputerName $computer -Pattern $hotfix.property.id -IncludeHidden

                        if ($exists.Summary -match "restart") {
                            $status = "This update requires a restart"
                        } else {
                            $status = "Install successful"
                        }
                        if ($HotfixId) {
                            $id = $HotfixId
                        } else {
                            $id = $guid
                        }
                        if ($id -eq "DAADB00F-DAAD-B00F-B00F-DAADB00FB00F") {
                            $id = $null
                        }
                        [pscustomobject]@{
                            ComputerName = $computer
                            Title        = $Title
                            ID           = $id
                            Status       = $Status
                            FileName     = $updatefile.Name
                        } | Select-DefaultView -Property ComputerName, Title, Status, FileName, Id
                    } catch {
                        if ("$PSItem" -match "Serialized XML is nested too deeply") {
                            Write-PSFMessage -Level Verbose -Message "Serialized XML is nested too deeply. Forcing output."
                            $exists = Get-KbInstalledUpdate -ComputerName $computer -HotfixId $hotfix.property.id

                            if ($exists.Summary -match "restart") {
                                $status = "This update requires a restart"
                            } else {
                                $status = "Install successful"
                            }
                            if ($HotfixId) {
                                $id = $HotfixId
                            } else {
                                $id = $guid
                            }

                            if ($id -eq "DAADB00F-DAAD-B00F-B00F-DAADB00FB00F") {
                                $id = $null
                            }

                            [pscustomobject]@{
                                ComputerName = $computer
                                Title        = $Title
                                ID           = $id
                                Status       = $Status
                                FileName     = $updatefile.Name
                            } | Select-DefaultView -Property ComputerName, Title, Status, FileName, Id
                        } else {
                            Stop-PSFFunction -Message "Failure on $computer" -ErrorRecord $_ -EnableException:$EnableException
                        }
                    }
                }
            }
        }
    }
    end {
        if ($warnatbottom) {
            Write-PSFMessage -Level Output -Message "Downloaded files may still exist on your local drive and other servers as well, in the Downloads directory."
            Write-PSFMessage -Level Output -Message "If you ran this as SYSTEM, the downloads will be in windows\system32\config\systemprofile."
        }
    }
}
# SIG # Begin signature block
# MIIjYAYJKoZIhvcNAQcCoIIjUTCCI00CAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDtmiiYaIF3nUiT
# PYp71j7HrkHuZnkwI3lnpa7BUp2duaCCHVkwggUaMIIEAqADAgECAhADBbuGIbCh
# Y1+/3q4SBOdtMA0GCSqGSIb3DQEBCwUAMHIxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAvBgNV
# BAMTKERpZ2lDZXJ0IFNIQTIgQXNzdXJlZCBJRCBDb2RlIFNpZ25pbmcgQ0EwHhcN
# MjAwNTEyMDAwMDAwWhcNMjMwNjA4MTIwMDAwWjBXMQswCQYDVQQGEwJVUzERMA8G
# A1UECBMIVmlyZ2luaWExDzANBgNVBAcTBlZpZW5uYTERMA8GA1UEChMIZGJhdG9v
# bHMxETAPBgNVBAMTCGRiYXRvb2xzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
# CgKCAQEAvL9je6vjv74IAbaY5rXqHxaNeNJO9yV0ObDg+kC844Io2vrHKGD8U5hU
# iJp6rY32RVprnAFrA4jFVa6P+sho7F5iSVAO6A+QZTHQCn7oquOefGATo43NAadz
# W2OWRro3QprMPZah0QFYpej9WaQL9w/08lVaugIw7CWPsa0S/YjHPGKQ+bYgI/kr
# EUrk+asD7lvNwckR6pGieWAyf0fNmSoevQBTV6Cd8QiUfj+/qWvLW3UoEX9ucOGX
# 2D8vSJxL7JyEVWTHg447hr6q9PzGq+91CO/c9DWFvNMjf+1c5a71fEZ54h1mNom/
# XoWZYoKeWhKnVdv1xVT1eEimibPEfQIDAQABo4IBxTCCAcEwHwYDVR0jBBgwFoAU
# WsS5eyoKo6XqcQPAYPkt9mV1DlgwHQYDVR0OBBYEFPDAoPu2A4BDTvsJ193ferHL
# 454iMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzB3BgNVHR8E
# cDBuMDWgM6Axhi9odHRwOi8vY3JsMy5kaWdpY2VydC5jb20vc2hhMi1hc3N1cmVk
# LWNzLWcxLmNybDA1oDOgMYYvaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL3NoYTIt
# YXNzdXJlZC1jcy1nMS5jcmwwTAYDVR0gBEUwQzA3BglghkgBhv1sAwEwKjAoBggr
# BgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAIBgZngQwBBAEw
# gYQGCCsGAQUFBwEBBHgwdjAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNl
# cnQuY29tME4GCCsGAQUFBzAChkJodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20v
# RGlnaUNlcnRTSEEyQXNzdXJlZElEQ29kZVNpZ25pbmdDQS5jcnQwDAYDVR0TAQH/
# BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAj835cJUMH9Y2pBKspjznNJwcYmOxeBcH
# Ji+yK0y4bm+j44OGWH4gu/QJM+WjZajvkydJKoJZH5zrHI3ykM8w8HGbYS1WZfN4
# oMwi51jKPGZPw9neGS2PXrBcKjzb7rlQ6x74Iex+gyf8z1ZuRDitLJY09FEOh0BM
# LaLh+UvJ66ghmfIyjP/g3iZZvqwgBhn+01fObqrAJ+SagxJ/21xNQJchtUOWIlxR
# kuUn9KkuDYrMO70a2ekHODcAbcuHAGI8wzw4saK1iPPhVTlFijHS+7VfIt/d/18p
# MLHHArLQQqe1Z0mTfuL4M4xCUKpebkH8rI3Fva62/6osaXLD0ymERzCCBTAwggQY
# oAMCAQICEAQJGBtf1btmdVNDtW+VUAgwDQYJKoZIhvcNAQELBQAwZTELMAkGA1UE
# BhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2lj
# ZXJ0LmNvbTEkMCIGA1UEAxMbRGlnaUNlcnQgQXNzdXJlZCBJRCBSb290IENBMB4X
# DTEzMTAyMjEyMDAwMFoXDTI4MTAyMjEyMDAwMFowcjELMAkGA1UEBhMCVVMxFTAT
# BgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEx
# MC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVkIElEIENvZGUgU2lnbmluZyBD
# QTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPjTsxx/DhGvZ3cH0wsx
# SRnP0PtFmbE620T1f+Wondsy13Hqdp0FLreP+pJDwKX5idQ3Gde2qvCchqXYJawO
# eSg6funRZ9PG+yknx9N7I5TkkSOWkHeC+aGEI2YSVDNQdLEoJrskacLCUvIUZ4qJ
# RdQtoaPpiCwgla4cSocI3wz14k1gGL6qxLKucDFmM3E+rHCiq85/6XzLkqHlOzEc
# z+ryCuRXu0q16XTmK/5sy350OTYNkO/ktU6kqepqCquE86xnTrXE94zRICUj6whk
# PlKWwfIPEvTFjg/BougsUfdzvL2FsWKDc0GCB+Q4i2pzINAPZHM8np+mM6n9Gd8l
# k9ECAwEAAaOCAc0wggHJMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQD
# AgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMDMHkGCCsGAQUFBwEBBG0wazAkBggrBgEF
# BQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEMGCCsGAQUFBzAChjdodHRw
# Oi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0Eu
# Y3J0MIGBBgNVHR8EejB4MDqgOKA2hjRodHRwOi8vY3JsNC5kaWdpY2VydC5jb20v
# RGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3JsMDqgOKA2hjRodHRwOi8vY3JsMy5k
# aWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3JsME8GA1UdIARI
# MEYwOAYKYIZIAYb9bAACBDAqMCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy5kaWdp
# Y2VydC5jb20vQ1BTMAoGCGCGSAGG/WwDMB0GA1UdDgQWBBRaxLl7KgqjpepxA8Bg
# +S32ZXUOWDAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzANBgkqhkiG
# 9w0BAQsFAAOCAQEAPuwNWiSz8yLRFcgsfCUpdqgdXRwtOhrE7zBh134LYP3DPQ/E
# r4v97yrfIFU3sOH20ZJ1D1G0bqWOWuJeJIFOEKTuP3GOYw4TS63XX0R58zYUBor3
# nEZOXP+QsRsHDpEV+7qvtVHCjSSuJMbHJyqhKSgaOnEoAjwukaPAJRHinBRHoXpo
# aK+bp1wgXNlxsQyPu6j4xRJon89Ay0BEpRPw5mQMJQhCMrI2iiQC/i9yfhzXSUWW
# 6Fkd6fp0ZGuy62ZD2rOwjNXpDd32ASDOmTFjPQgaGLOBm0/GkxAG/AeB+ova+YJJ
# 92JuoVP6EpQYhS6SkepobEQysmah5xikmmRR7zCCBY0wggR1oAMCAQICEA6bGI75
# 0C3n79tQ4ghAGFowDQYJKoZIhvcNAQEMBQAwZTELMAkGA1UEBhMCVVMxFTATBgNV
# BAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEkMCIG
# A1UEAxMbRGlnaUNlcnQgQXNzdXJlZCBJRCBSb290IENBMB4XDTIyMDgwMTAwMDAw
# MFoXDTMxMTEwOTIzNTk1OVowYjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lD
# ZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGln
# aUNlcnQgVHJ1c3RlZCBSb290IEc0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
# CgKCAgEAv+aQc2jeu+RdSjwwIjBpM+zCpyUuySE98orYWcLhKac9WKt2ms2uexuE
# DcQwH/MbpDgW61bGl20dq7J58soR0uRf1gU8Ug9SH8aeFaV+vp+pVxZZVXKvaJNw
# wrK6dZlqczKU0RBEEC7fgvMHhOZ0O21x4i0MG+4g1ckgHWMpLc7sXk7Ik/ghYZs0
# 6wXGXuxbGrzryc/NrDRAX7F6Zu53yEioZldXn1RYjgwrt0+nMNlW7sp7XeOtyU9e
# 5TXnMcvak17cjo+A2raRmECQecN4x7axxLVqGDgDEI3Y1DekLgV9iPWCPhCRcKtV
# gkEy19sEcypukQF8IUzUvK4bA3VdeGbZOjFEmjNAvwjXWkmkwuapoGfdpCe8oU85
# tRFYF/ckXEaPZPfBaYh2mHY9WV1CdoeJl2l6SPDgohIbZpp0yt5LHucOY67m1O+S
# kjqePdwA5EUlibaaRBkrfsCUtNJhbesz2cXfSwQAzH0clcOP9yGyshG3u3/y1Yxw
# LEFgqrFjGESVGnZifvaAsPvoZKYz0YkH4b235kOkGLimdwHhD5QMIR2yVCkliWzl
# DlJRR3S+Jqy2QXXeeqxfjT/JvNNBERJb5RBQ6zHFynIWIgnffEx1P2PsIV/EIFFr
# b7GrhotPwtZFX50g/KEexcCPorF+CiaZ9eRpL5gdLfXZqbId5RsCAwEAAaOCATow
# ggE2MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFOzX44LScV1kTN8uZz/nupiu
# HA9PMB8GA1UdIwQYMBaAFEXroq/0ksuCMS1Ri6enIZ3zbcgPMA4GA1UdDwEB/wQE
# AwIBhjB5BggrBgEFBQcBAQRtMGswJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRp
# Z2ljZXJ0LmNvbTBDBggrBgEFBQcwAoY3aHR0cDovL2NhY2VydHMuZGlnaWNlcnQu
# Y29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNydDBFBgNVHR8EPjA8MDqgOKA2
# hjRodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290
# Q0EuY3JsMBEGA1UdIAQKMAgwBgYEVR0gADANBgkqhkiG9w0BAQwFAAOCAQEAcKC/
# Q1xV5zhfoKN0Gz22Ftf3v1cHvZqsoYcs7IVeqRq7IviHGmlUIu2kiHdtvRoU9BNK
# ei8ttzjv9P+Aufih9/Jy3iS8UgPITtAq3votVs/59PesMHqai7Je1M/RQ0SbQyHr
# lnKhSLSZy51PpwYDE3cnRNTnf+hZqPC/Lwum6fI0POz3A8eHqNJMQBk1RmppVLC4
# oVaO7KTVPeix3P0c2PR3WlxUjG/voVA9/HYJaISfb8rbII01YBwCA8sgsKxYoA5A
# Y8WYIsGyWfVVa88nq2x2zm8jLfR+cWojayL/ErhULSd+2DrZ8LaHlv1b0VysGMNN
# n3O3AamfV6peKOK5lDCCBq4wggSWoAMCAQICEAc2N7ckVHzYR6z9KGYqXlswDQYJ
# KoZIhvcNAQELBQAwYjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IElu
# YzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNlcnQg
# VHJ1c3RlZCBSb290IEc0MB4XDTIyMDMyMzAwMDAwMFoXDTM3MDMyMjIzNTk1OVow
# YzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQD
# EzJEaWdpQ2VydCBUcnVzdGVkIEc0IFJTQTQwOTYgU0hBMjU2IFRpbWVTdGFtcGlu
# ZyBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMaGNQZJs8E9cklR
# VcclA8TykTepl1Gh1tKD0Z5Mom2gsMyD+Vr2EaFEFUJfpIjzaPp985yJC3+dH54P
# Mx9QEwsmc5Zt+FeoAn39Q7SE2hHxc7Gz7iuAhIoiGN/r2j3EF3+rGSs+QtxnjupR
# PfDWVtTnKC3r07G1decfBmWNlCnT2exp39mQh0YAe9tEQYncfGpXevA3eZ9drMvo
# hGS0UvJ2R/dhgxndX7RUCyFobjchu0CsX7LeSn3O9TkSZ+8OpWNs5KbFHc02DVzV
# 5huowWR0QKfAcsW6Th+xtVhNef7Xj3OTrCw54qVI1vCwMROpVymWJy71h6aPTnYV
# VSZwmCZ/oBpHIEPjQ2OAe3VuJyWQmDo4EbP29p7mO1vsgd4iFNmCKseSv6De4z6i
# c/rnH1pslPJSlRErWHRAKKtzQ87fSqEcazjFKfPKqpZzQmiftkaznTqj1QPgv/Ci
# PMpC3BhIfxQ0z9JMq++bPf4OuGQq+nUoJEHtQr8FnGZJUlD0UfM2SU2LINIsVzV5
# K6jzRWC8I41Y99xh3pP+OcD5sjClTNfpmEpYPtMDiP6zj9NeS3YSUZPJjAw7W4oi
# qMEmCPkUEBIDfV8ju2TjY+Cm4T72wnSyPx4JduyrXUZ14mCjWAkBKAAOhFTuzuld
# yF4wEr1GnrXTdrnSDmuZDNIztM2xAgMBAAGjggFdMIIBWTASBgNVHRMBAf8ECDAG
# AQH/AgEAMB0GA1UdDgQWBBS6FtltTYUvcyl2mi91jGogj57IbzAfBgNVHSMEGDAW
# gBTs1+OC0nFdZEzfLmc/57qYrhwPTzAOBgNVHQ8BAf8EBAMCAYYwEwYDVR0lBAww
# CgYIKwYBBQUHAwgwdwYIKwYBBQUHAQEEazBpMCQGCCsGAQUFBzABhhhodHRwOi8v
# b2NzcC5kaWdpY2VydC5jb20wQQYIKwYBBQUHMAKGNWh0dHA6Ly9jYWNlcnRzLmRp
# Z2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRSb290RzQuY3J0MEMGA1UdHwQ8MDow
# OKA2oDSGMmh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRS
# b290RzQuY3JsMCAGA1UdIAQZMBcwCAYGZ4EMAQQCMAsGCWCGSAGG/WwHATANBgkq
# hkiG9w0BAQsFAAOCAgEAfVmOwJO2b5ipRCIBfmbW2CFC4bAYLhBNE88wU86/GPvH
# UF3iSyn7cIoNqilp/GnBzx0H6T5gyNgL5Vxb122H+oQgJTQxZ822EpZvxFBMYh0M
# CIKoFr2pVs8Vc40BIiXOlWk/R3f7cnQU1/+rT4osequFzUNf7WC2qk+RZp4snuCK
# rOX9jLxkJodskr2dfNBwCnzvqLx1T7pa96kQsl3p/yhUifDVinF2ZdrM8HKjI/rA
# J4JErpknG6skHibBt94q6/aesXmZgaNWhqsKRcnfxI2g55j7+6adcq/Ex8HBanHZ
# xhOACcS2n82HhyS7T6NJuXdmkfFynOlLAlKnN36TU6w7HQhJD5TNOXrd/yVjmScs
# PT9rp/Fmw0HNT7ZAmyEhQNC3EyTN3B14OuSereU0cZLXJmvkOHOrpgFPvT87eK1M
# rfvElXvtCl8zOYdBeHo46Zzh3SP9HSjTx/no8Zhf+yvYfvJGnXUsHicsJttvFXse
# GYs2uJPU5vIXmVnKcPA3v5gA3yAWTyf7YGcWoWa63VXAOimGsJigK+2VQbc61RWY
# MbRiCQ8KvYHZE/6/pNHzV9m8BPqC3jLfBInwAM1dwvnQI38AC+R2AibZ8GV2QqYp
# hwlHK+Z/GqSFD/yYlvZVVCsfgPrA8g4r5db7qS9EFUrnEw4d2zc4GqEr9u3WfPww
# ggbAMIIEqKADAgECAhADyzT9Pf8SETOf8HxLIVfHMA0GCSqGSIb3DQEBCwUAMGMx
# CzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMy
# RGlnaUNlcnQgVHJ1c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcg
# Q0EwHhcNMjIwODMwMDAwMDAwWhcNMjMwODI5MjM1OTU5WjBGMQswCQYDVQQGEwJV
# UzERMA8GA1UEChMIRGlnaUNlcnQxJDAiBgNVBAMTG0RpZ2lDZXJ0IFRpbWVzdGFt
# cCAyMDIyIC0gMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAM/spSY6
# xqnya7uNwQ2a26HoFIV0MxomrNAcVR4eNm28klUMYfSdCXc9FZYIL2tkpP0GgxbX
# kZI4HDEClvtysZc6Va8z7GGK6aYo25BjXL2JU+A6LYyHQq4mpOS7eHi5ehbhVsbA
# umRTuyoW51BIu4hpDIjG8b7gL307scpTjUCDHufLckkoHkyAHoVW54Xt8mG8qjoH
# ffarbuVm3eJc9S/tjdRNlYRo44DLannR0hCRRinrPibytIzNTLlmyLuqUDgN5YyU
# XRlav/V7QG5vFqianJVHhoV5PgxeZowaCiS+nKrSnLb3T254xCg/oxwPUAY3ugjZ
# Naa1Htp4WB056PhMkRCWfk3h3cKtpX74LRsf7CtGGKMZ9jn39cFPcS6JAxGiS7uY
# v/pP5Hs27wZE5FX/NurlfDHn88JSxOYWe1p+pSVz28BqmSEtY+VZ9U0vkB8nt9Kr
# FOU4ZodRCGv7U0M50GT6Vs/g9ArmFG1keLuY/ZTDcyHzL8IuINeBrNPxB9Thvdld
# S24xlCmL5kGkZZTAWOXlLimQprdhZPrZIGwYUWC6poEPCSVT8b876asHDmoHOWIZ
# ydaFfxPZjXnPYsXs4Xu5zGcTB5rBeO3GiMiwbjJ5xwtZg43G7vUsfHuOy2SJ8bHE
# uOdTXl9V0n0ZKVkDTvpd6kVzHIR+187i1Dp3AgMBAAGjggGLMIIBhzAOBgNVHQ8B
# Af8EBAMCB4AwDAYDVR0TAQH/BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAg
# BgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwHwYDVR0jBBgwFoAUuhbZ
# bU2FL3MpdpovdYxqII+eyG8wHQYDVR0OBBYEFGKK3tBh/I8xFO2XC809KpQU31Kc
# MFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdp
# Q2VydFRydXN0ZWRHNFJTQTQwOTZTSEEyNTZUaW1lU3RhbXBpbmdDQS5jcmwwgZAG
# CCsGAQUFBwEBBIGDMIGAMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2Vy
# dC5jb20wWAYIKwYBBQUHMAKGTGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9E
# aWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZTSEEyNTZUaW1lU3RhbXBpbmdDQS5jcnQw
# DQYJKoZIhvcNAQELBQADggIBAC0UyaEGSS3dimxaHgXjrMnYnjeKsKYhIj9EyjE9
# ywwM33xT5ZRqdiX3Isk7nEIElPWCRN5u4oTo7k5EGGktx3ZsrHpzf0siEEmEdDfy
# gtNBlXYxLvlZab8HVrslWfexM+66XRCFK19PgSnudu0gC3XaxWbC6eAeWmgBTLRk
# tDRpqbY9fj1d6REtuXxf4RNrN0MDT+kVDdt1BVTHDTlfGDbA6HAXR1Vc+khF8cv4
# RMJ8vvP3p6z05qFttPe3RMWPCC+d8hKtJI+2C3hBwdKChzJizkfq60Vrqqj+dEeB
# nrUYhUcYIIz6WeVYk72r/31a9SowYPuTzNCktU59LF6Y2/bMPIpHeHhsBAvg2RMx
# DzH4TfzgKkGM8F8VDpTAKUXe8vlzzsNjJ4m+oeGi72Kj6if/M07iiT4kMEQV5Fg8
# BotKdIqx7a1Cf+aqpZq5+DAcFhPwo4uoKtSLAWY0aIACxRKSFqIHngiuc2t9n+vB
# /oM/rtlQNnnlt8E2hvC3yQl5+M/7sqzX4vI3BBv6ASmOsDaYOGrb90BA77kpxccg
# avKscb/UdmJ+yGZjMyuuUzjPpKpGxMG95S9ATieDVuDFi68taSY81PJVmxBD/MrB
# bfTZ9JBLS5F1s0ecKEr6OOY1PvLIry+8TrgnFUP5KT019GjiRV2GVCOBx9aBB9M+
# oTliMYIFXTCCBVkCAQEwgYYwcjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lD
# ZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTExMC8GA1UEAxMoRGln
# aUNlcnQgU0hBMiBBc3N1cmVkIElEIENvZGUgU2lnbmluZyBDQQIQAwW7hiGwoWNf
# v96uEgTnbTANBglghkgBZQMEAgEFAKCBhDAYBgorBgEEAYI3AgEMMQowCKACgACh
# AoAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAM
# BgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJBDEiBCACcUlgEPv2XJSElGAOw940MWPH
# evs6Rbvw7VUJJzj0LDANBgkqhkiG9w0BAQEFAASCAQBtsoHq5D3lBFHQ9HwEajjT
# o+MLQDksV5oehALs6Q3VoENzI07F0UXusbkoCGFGPAlkzuJgSs3Ywqp5scJS3Ie3
# lA6f8ltvEAfOGZuMBqMoX9vvGwzsgseEe2kBLmzpQm8b8EM0fW/KbRKoRILpdszs
# +X5vR6xzG3PZO/8P3M/eMCqk5+VdsyAGnDZT/cCX1yhwPuAwsF+CwsIXiBMftHfs
# PSrQHTuLEPeR+Gm9Gbf8+eM+cAxlBWlLxR7x5LZmiJnwP//fCEHZQx8YmBdPZoHW
# v+J+Pt/OmD5n/ucujL0+wZu5MS0ql4o05CZl5akNmixVvFKVFwSQZG6nSAMNwpIl
# oYIDIDCCAxwGCSqGSIb3DQEJBjGCAw0wggMJAgEBMHcwYzELMAkGA1UEBhMCVVMx
# FzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBUcnVz
# dGVkIEc0IFJTQTQwOTYgU0hBMjU2IFRpbWVTdGFtcGluZyBDQQIQA8s0/T3/EhEz
# n/B8SyFXxzANBglghkgBZQMEAgEFAKBpMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0B
# BwEwHAYJKoZIhvcNAQkFMQ8XDTIyMDkxNTIzMTQ1OFowLwYJKoZIhvcNAQkEMSIE
# IHLpoEsBO+GzguT6tqI7q1USA9Fqie0rgf+0CxaTV0kiMA0GCSqGSIb3DQEBAQUA
# BIICAAjOXbNHesrU4qq307eMqwn32QE33VLeDyOQkOtdNJOze3dovftP9IlVLVDn
# SLUPsQz6QL2vlA07o4TiX6h0ccG3tqecexCwLCHDDVOAxFUoKe+bUjXqRux8O5+x
# McAsSPXZ3eVTGc/TAlQax4bfGwS5OK5Dbrn31ZMnZ85siIRM6JgZvZYN6oJPObx1
# KegIbo2GF7PSLeEZDdVB4188uTbdfKbpvbJL5XmAf6ps5w8ZozUjj8oN2Wluj97Q
# uOLJOkzZRjpsiM8fSR1zskePEB/8kdZCDcdoD9Q9YBn1JWyhHIijW+17kLbpfqx7
# D1uloap9N21BY0A9PWKmMZt4+shZnP4KWyNbUBQ9/2g0GeWAllcZSfE10y/uT38G
# Et9I6tyqFZNQ+URMOCKWapswXkVjEXT+xkhpNjRRVwOG4dKxlcl0dTAFpauErNaF
# siZhhw9pybaxZKO/WcMx6AMlrpDo6NAeM2Mp3M6EmqOh2xgcY/Bt0kMYXIQzphQG
# fFpcSnM09WZX59Lg+/sB5KNd7kmMdqyjd1Ru4TJzOMXia0/upa5w+374M6Htsftx
# jz2uzynWC365JEohidsqmmKKWvudD26idwQ9zu/hUwqBH+eur0gCDHtgjDmYXPdE
# lc1mA3c8FFL0zvkfI35/RSGd6D76rPuMdkgNpe09QiFaonlO
# SIG # End signature block