Public/Import-WsusUpdate.ps1

<#
    .SYNOPSIS
    Imports updates from the Microsoft Update Catalog into your local WSUS server.
 
    .DESCRIPTION
    The Import-WsusUpdate cmdlet imports updates from the Microsoft Update Catalog into your local WSUS server.
 
    By default, Import-WsusUpdate will download the update files required for this update to a temporary folder in the TEMP folder (%TEMP%\Import-WsusUpdate) and import them into the WSUS server.
 
    The update files will be removed from the temporary folder after successfully importing the update into the WSUS server.
 
    The download path may be changed to a different location using the -DownloadPath parameter of the cmdlet. When using the -DownloadPath parameter, the update files in the folder will not be removed after import. This can be useful for downloading the update files once, and importing them on multiple WSUS servers.
 
    .EXAMPLE
    Import-WsusUpdate "0373ad3c-3f8f-4123-81f8-6d3d8cd86f6d"
    Description: Downloads the update files for the update and imports the metadata for the update into the local WSUS server.
 
    The update files will be stored on the local WSUS server (similar to how WSUS will download the files when updates are approved/required).
 
    The UpdateID of an update on the Microsoft Update Catalog can be found on the Update Details page for the update.
 
    .EXAMPLE
    Import-WsusUpdate "0373ad3c-3f8f-4123-81f8-6d3d8cd86f6d" -MetadataOnly
    Description: Imports only the metadata for the update into the local WSUS server.
 
    The update files will be downloaded later by WSUS based on the WSUS server's settings.
 
    The UpdateID of an update on the Microsoft Update Catalog can be found on the Update Details page for the update.
 
    .EXAMPLE
    Import-WsusUpdate -KB "KB5026454"
    Description: Searches the Microsoft Update Catalog for the update with KB number 5026454.
 
    If only one result (update) is found, downloads the update files for the update and imports the metadata for the update into the local WSUS server. The update files will be stored on the local WSUS server (similar to how WSUS will download the files when updates are approved/required).
 
    If more than one result (update) is found, the cmdlet will halt execution. The -Filter parameter will need to be used to further refine the search results, or one of the architecture parameters (-x86, -x64, -ARM64) may be used.
 
    Alternatively, the UpdateID can be used instead of filtering the results, and may be required if multiple updates with the exact same title have been released (e.g.: 2023-07 Cumulative Update Preview for .NET Framework 3.5 and 4.8.1 for Windows 11, version 22H2 for x64 (KB5028017)).
 
    .EXAMPLE
    Import-WsusUpdate -KB "KB5026454" -MetadataOnly
    Description: Searches the Microsoft Update Catalog for the update with KB number 5026454.
 
    If only one result (update) is found, imports only the metadata for the update into the local WSUS server. The update files will be downloaded later by WSUS based on the WSUS server's settings.
 
    If more than one result (update) is found, the cmdlet will halt execution. The -Filter parameter will need to be used to further refine the search results, or one of the architecture parameters (-x86, -x64, -ARM64) may be used.
 
    Alternatively, the UpdateID can be used instead of filtering the results, and may be required if multiple updates with the exact same title have been released (e.g.: 2023-07 Cumulative Update Preview for .NET Framework 3.5 and 4.8.1 for Windows 11, version 22H2 for x64 (KB5028017)).
 
    .EXAMPLE
    Import-WsusUpdate -KB KB5037422,KB5037425,KB5037423,KB5037426 -MetadataOnly -Filter "Server"
    Description: Searches the Microsoft Update Catalog for the updates with KB numbers provided, and the filters "Server".
 
    -Filter "Server" is used to filter the search results returned from the Microsoft Update Catalog for EACH KB. This is used as KB5037425, KB5037423, and KB5037426 have been released for multiple Windows versions (e.g.: Windows 10 Version 1809 and Windows Server 2019), but we are only interested in importing the version for Windows Server.
 
    Only the metadata for the updates will be imported into the local WSUS server.
 
    The updates' files will be downloaded later by WSUS based on the WSUS server's settings.
 
    .EXAMPLE
    Import-WsusUpdate -KB "KB5029244" -MetadataOnly -x64 -Filter "21H2","-Dynamic"
    Description: Searches the Microsoft Update Catalog for the update with KB number KB5029244, and the filters "21H2", "-Dynamic" and "x64".
 
    Only the metadata for the update will be imported into the local WSUS server.
 
    The update files will be downloaded later by WSUS based on the WSUS server's settings.
 
    .EXAMPLE
    Import-WsusUpdate "0373ad3c-3f8f-4123-81f8-6d3d8cd86f6d" -DownloadPath "C:\MUCatalogUpdateFiles"
    Description: Downloads the update files for the update to C:\MUCatalogUpdateFiles and imports the metadata for the update into the local WSUS server.
 
    The update files will be stored on the local WSUS server (similar to how WSUS will download the files when updates are approved/required).
 
    If the update files are already present in the folder specified by the -DownloadPath parameter, the download(s) for the existing file(s) will be skipped. The update files will not be automatically removed after importing them into the WSUS server.
 
    The UpdateID of an update on the Microsoft Update Catalog can be found on the Update Details page for the update.
 
    .EXAMPLE
    Import-WsusUpdate "0373ad3c-3f8f-4123-81f8-6d3d8cd86f6d" -DownloadPath "C:\MUCatalogUpdateFiles" -DownloadInParallel
    Description: Downloads the update files for the update to C:\MUCatalogUpdateFiles and imports the metadata for the update into the local WSUS server.
 
    The update files will be stored on the local WSUS server (similar to how WSUS will download the files when updates are approved/required).
 
    If the update files are already present in the folder specified by the -DownloadPath parameter, the download(s) for the existing file(s) will be skipped. The update files will not be automatically removed after importing them into the WSUS server.
 
    The update files will be downloaded in parallel, with a maximum of 4 active downloads active at once.
 
    The UpdateID of an update on the Microsoft Update Catalog can be found on the Update Details page for the update.
 
    .NOTES
    The Import-WsusUpdate cmdlet requires an internet connection to import update metadata from the Microsoft Update upstream server.
 
    The Import-WsusUpdate cmdlet will not work on WSUS servers syncing from an upstream server other than Microsoft Update, unless the AllowCatalogImportOnDSS registry key has been set. This is a restriction from the WSUS API.
 
    If the WSUS server is configured to use a proxy, the Import-WsusUpdate cmdlet will respect the WSUS proxy configuration and use the proxy when connecting to the Internet and downloading update files when importing updates.
 
    Update files are downloaded one at a time by default, unless the -DownloadInParallel switch is passed.
 
    Currently remote servers are not supported, but this functionality is planned for subsequent releases.
 
    .INPUTS
    This cmdlet does not currently support piping input.
 
    .OUTPUTS
    Nothing.
#>

function Import-WsusUpdate {
    [CmdletBinding(DefaultParameterSetName = "UpdateID")]
    param (
        [Parameter(Mandatory, ParameterSetName = "UpdateID", Position = 0,
            HelpMessage = "The UpdateID (GUID) of the Microsoft Update Catalog update to import.")]
        [ValidatePattern("[a-zA-Z\d]{8}-(?:[a-zA-Z\d]{4}-){3}[a-zA-Z\d]{12}")]
        [string]
        $UpdateID,

        [Parameter(Mandatory, ParameterSetName = "KB",
            HelpMessage = "The KB of the update(s) to download. Since multiple updates may share the same KB (e.g.: 64-bit and 32-bit release of the same KB) additional parameters may need to be used to narrow down the search. When providing multiple KBs (e.g.: KB5037422,KB5037425) additional parameters like -Filter will apply to all KBs provided.")]
        [ValidatePattern("^KB\d{7}$")]
        [string[]]
        $KB,

        [Parameter(Mandatory, ParameterSetName = "Uri",
            HelpMessage = "The URI in the address bar for this update. It must contain the following text: ScopedViewInline.aspx?updateid=")]
        [ValidatePattern(".*?ScopedViewInline\.aspx\?updateid=([a-zA-Z\d]{8}-(?:[a-zA-Z\d]{4}-){3}[a-zA-Z\d]{12}).*")]
        [string]
        $Uri,

        [Parameter(ParameterSetName = "KB",
            HelpMessage = "Any additional keywords to include in the search to filter matching updates (e.g.: 'x86-based'). When providing multiple update KBs to import the additional keywords will be used when searching for every KB provided, so it is important to use keywords that will not remove results from the other KBs that are being imported. If you cannot find a common keyword shared between KBs then you will need to import the KBs one at a time.")]
        [AllowNull()]
        [AllowEmptyCollection()]
        [string[]]
        $Filter,

        [Parameter(ParameterSetName = "KB",
            HelpMessage = "Adds 'x64' to the list of filters used when searching for updates to filter out non-64bit updates. This is the equivalent of adding 'x64' to -Filter.")]
        [Alias("64Bit")]
        [switch]
        $x64,

        [Parameter(ParameterSetName = "KB",
            HelpMessage = "Adds 'x86' to the list of filters used when searching for updates to filter out non-32bit updates. This is the equivalent of adding 'x86' to -Filter.")]
        [Alias("32Bit")]
        [switch]
        $x86,

        [Parameter(ParameterSetName = "KB",
            HelpMessage = "Adds 'ARM64' to the list of filters used when searching for updates to filter out non-ARM64 updates. This is the equivalent of adding 'ARM64' to -Filter.")]
        [switch]
        $ARM64,

        [Parameter(HelpMessage = "The path to save the update files required by the update. When specified, the downloaded update files will not be removed after being successfully imported into the WSUS server, allowing you to import the files on another WSUS server without re-downloading the update files.")]
        [string]
        $DownloadPath,

        [Parameter(HelpMessage = "If update files should be downloaded simultaneously. By default update files are downloaded one at a time.")]
        [switch]
        $DownloadInParallel,

        [Parameter(HelpMessage = "If only the metadata of the update should be imported into the WSUS server. The WSUS server will determine when to download the files required for this file (e.g.: when the update is approved).")]
        [Alias("SkipDownload")]
        [switch]
        $MetadataOnly,

        [Parameter(HelpMessage = "For diagnosing errors. A PowerShell transcript will be started for troubleshooting, and relevant WSUS/system information will be displayed at the end of the script's execution.")]
        [switch]
        $HelpMe,

        [Parameter(HelpMessage = "Removes the larger AJ Tek logo from the script's output.")]
        [switch]
        $NoLogo
    )

    if ($IwuImportContinue -ne "SilentlyContinue" -and $Script:IwuJustUpdated) {
        Write-Error -Message "Import-WsusUpdate just updated! Please restart your PowerShell session. For scripts, the `$IwuImportContinue script/global variable (`$Script:IwuImportContinue or `$Global:IwuImportContinue) can be set to 'SilentlyContinue' remove this check." `
            -Category InvalidOperation `
            -ErrorId "IwuRestartOrReimportNeeded"
        return
    }

    if ($PSVersionTable.PSEdition -eq "Core") {
        Write-Error -Message "PowerShell Core is not supported. Please use Windows PowerShell to run this cmdlet." `
            -Category InvalidOperation `
            -ErrorId "PowerShellCoreIsNotSupported"
        return
    }

    if (!$Script:WarnedPSVersion -and $PSVersionTable.PSVersion.Major -lt 4) {
        Write-Warning "Only PowerShell 4.0 and PowerShell 5.1 are officially supported."
        Write-Warning "It is possible that the cmdlet will work on other PowerShell versions, but it is highly recommended that you use one of the officially supported versions."

        # We shouldn't pester the user - only warn them once.
        $Script:WarnedPSVersion = $true
    }

    if ([System.Environment]::Is64BitOperatingSystem -and ![System.Environment]::Is64BitProcess) {
        Write-Warning "Running in a 32-bit process on a 64-bit machine. If you encounter errors, please try using the 64-bit version of PowerShell to run the cmdlet."
    }

    # 3072 and 12288 are used for comparisons as older .NET framework versions do not have TLS11, TLS12, TLS13 available
    # in the [SecurityProtocolType] enum.
    $currentSecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol
    $warnSecurityProtocolAddendum = ""

    if ($currentSecurityProtocol -eq 0) {
        $warnSecurityProtocolAddendum = " This may be caused by [System.Net.ServicePointManager]::SecurityProtocol being set to SystemDefault, and the system default security protocol not supporting TLS 1.2 or higher."
    } elseif (!($currentSecurityProtocol -band 3072 <# TLS12 #>) -and !($currentSecurityProtocol -band 12288 <# TLS13 #>)) {
        $warnSecurityProtocolAddendum = " This may be caused by [System.Net.ServicePointManager]::SecurityProtocol not supporting TLS 1.2 or higher."
    }

    try {
        $wsus = $null
        $wsusConfig = $null
        $removeFilesAfterImport = $false

        if ($HelpMe) {
            if ([string]::IsNullOrWhiteSpace($env:TEMP)) {
                Write-Warning "Environmental TEMP variable was empty: starting HelpMe transcript in its default location."
                Start-Transcript -ErrorAction Stop
            } else {
                Start-Transcript -Path "${env:TEMP}\Import-WsusUpdate-$(Get-Date -f "yyyy.MM.dd-HH.mm.ss")-HelpMe.txt" -ErrorAction Stop
            }

            $VerbosePreference = "Continue"
            Write-Verbose "Verbose output will be displayed since we are in a HelpMe context."
        }

        try {
            # Ensure we are executing from an elevated process.
            if (!(TestElevated)) {
                Write-Error -Message "This cmdlet must be ran as an administrator." `
                    -Category PermissionDenied `
                    -ErrorId "CmdletRequiresElevation" `
                    -RecommendedAction "Run the command again from an elevated PowerShell window."
                return
            }

            if ($NoLogo) {
                Write-Output "Brought to you by: AJ Tek Corporation"
            } else {
                Write-Output "============================="
                Write-Output "Brought to you by:"
                Write-Output "AJ Tek Corporation"
                Write-Output "https://www.ajtek.ca"
                Write-Output "============================="
            }

            if (!$MetadataOnly) {
                # Ensure that we have a valid download path to save the update files to.
                if (!$PSBoundParameters.ContainsKey("DownloadPath")) {
                    $removeFilesAfterImport = $true
                    Write-Verbose "Using temporary folder to store update files since user didn't provide a download path."
                    $tempPath = $env:TEMP
                    $DownloadPath = Resolve-Path -Path $tempPath -ErrorAction "SilentlyContinue"

                    if ([string]::IsNullOrWhiteSpace($DownloadPath)) {
                        Write-Error -Message "The -DownloadPath parameter was not passed and the environmental TEMP variable ($tempPath) didn't point to an existing folder. Please use the -DownloadPath parameter and provide a path to a folder instead." `
                            -Category "ObjectNotFound" `
                            -ErrorId "EnvTempFolderNotFound" `
                            -TargetObject $tempPath
                        return
                    }

                    $DownloadPath = Join-Path $DownloadPath "Import-WsusUpdate"
                }

                if (!(Test-Path $DownloadPath)) {
                    Write-Verbose "Download path doesn't exist; creating it now..."

                    try {
                        [void](New-Item -Path $DownloadPath -ItemType Directory -ErrorAction Stop)
                    } catch {
                        Write-Warning "Couldn't create the download folder at $DownloadPath!"
                        Write-Warning "Please manually create this folder."
                        throw
                    }
                }

                Write-Output "Update files will be saved in $DownloadPath."
            }
        } catch {
            Write-Warning "Failed cmdlet startup with the following error:"
            throw
        }

        try {
            # Connect to the local WSUS server and retrieve its configuration settings.
            Write-Output "Connecting to the local WSUS server..."

            if ($null -ne (Get-Command -Name "Get-WsusServer" -ErrorAction Ignore)) {
                Write-Verbose "Connecting to the local WSUS server via the Get-WsusServer cmdlet."
                $wsus = Get-WsusServer -ErrorAction Stop
            } else {
                Write-Verbose "Get-WsusServer cmdlet was not available. Connecting to the local WSUS server via AdminProxy::GetUpdateServer."
                [void]([System.Reflection.Assembly]::LoadWithPartialName("Microsoft.UpdateServices.Administration"))
                $wsus = [Microsoft.UpdateServices.Administration.AdminProxy]::GetUpdateServer()
            }

            Write-Verbose "Retrieving WSUS configuration..."
            $wsusConfig = $wsus.GetConfiguration()

            if (!$wsusConfig.SyncFromMicrosoftUpdate) {
                $allowCatalogImportOnDSS = Get-ItemProperty -Path "HKLM:\Software\Microsoft\Update Services\Server\Setup" `
                    -Name "AllowCatalogImportOnDSS" `
                    -ErrorAction SilentlyContinue
                $allowCatalogImportOnDSS32 = Get-ItemProperty -Path "HKLM:\Software\Wow6432Node\Microsoft\Update Services\Server\Setup" `
                    -Name "AllowCatalogImportOnDSS" `
                    -ErrorAction SilentlyContinue

                if ($null -eq $allowCatalogImportOnDSS -and $null -eq $allowCatalogImportOnDSS32) {
                    Write-Error -Message "Cannot import updates as WSUS is syncing from an upstream server other than Microsoft Update." `
                        -Category InvalidOperation `
                        -ErrorId "CouldNotImportUpdateUSSNotMicrosoftUpdate"
                    return
                } else {
                    Write-Verbose "AllowCatalogImportOnDSS registry key exists; ignoring the fact that WSUS is set to sync from an upstream other than Microsoft Update."
                }
            }

            if (!(InitializeProxySettings -WsusConfig $wsusConfig -ErrorAction Stop)) {
                Write-Error -Message "WSUS proxy settings are set to an unsupported configuration." `
                    -Category InvalidOperation `
                    -ErrorId "CouldNotValidateProxySettings"
                return
            }
        } catch {
            Write-Warning "Failed establishing a valid connection to the WSUS server!"
            throw
        }

        $warnSchUseStrongCrypto = $false
        $warnSchUseStrongCrypto32 = $false
        $schUseStrongCrypto = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\.NETFramework\v4.0.30319" -Name "SchUseStrongCrypto" -ErrorAction Ignore

        if ($null -eq $schUseStrongCrypto -or $schUseStrongCrypto.SchUseStrongCrypto -ne "1") {
            Write-Verbose "SchUseStrongCrypto registry entry was missing or not set to its recommended value."
            $warnSchUseStrongCrypto = $true
        }

        if (Test-Path "HKLM:\SOFTWARE\Wow6432Node") {
            $schUseStrongCrypto32 = Get-ItemProperty -Path "HKLM:\SOFTWARE\Wow6432Node\Microsoft\.NetFramework\v4.0.30319" -Name "SchUseStrongCrypto" -ErrorAction Ignore

            if ($null -eq $schUseStrongCrypto32 -or $schUseStrongCrypto32.SchUseStrongCrypto -ne "1") {
                Write-Verbose "SchUseStrongCrypto registry entry (32-bit) was missing or not set to its recommended value."
                $warnSchUseStrongCrypto32 = $true
            }
        }

        if ($warnSchUseStrongCrypto -or $warnSchUseStrongCrypto32) {
            if ($warnSchUseStrongCrypto -and $warnSchUseStrongCrypto32) {
                $warnSchUseStrongCryptoWording = "both the 64-bit and 32-bit .NET Framework registry keys"
            } elseif ($warnSchUseStrongCrypto32) {
                $warnSchUseStrongCryptoWording = "the 32-bit .NET Framework registry key"
            } else {
                $warnSchUseStrongCryptoWording = "the .NET Framework registry key"
            }

            Write-Warning "The SchUseStrongCrypto registry entry was missing in $warnSchUseStrongCryptoWording. This will cause networking-related issues when executing, and it is required that you set the SchUseStrongCrypto registry value to 1. This enforces the .NET Framework to use TLS 1.2 for network communication.`r`n
            If you were already subscribed to WSUS Automated Maintenance (WAM) ©, this would have been brought to your attention already as WAM proactively checks your environment for optimizations, and issues on every single run. It then gives you the information and commands to perform the suggested actions.`r`n
            For WAM users, if you ignored the notification all this time, you must now adjust this setting for you to import updates into WSUS. This can be accomplished by opening the WAM Shell and running:`r`n
            Set-SchUseStrongCrypto -Value 1"

        }

        $updateIdKbMap = @{}
        $targetUpdateIds = New-Object -TypeName "System.Collections.Generic.List[string]"

        if ($PSBoundParameters.ContainsKey("KB")) {
            $mergedFilterList = New-Object -TypeName "System.Collections.Generic.List[string]"

            if ($x86) { [void]($mergedFilterList.Add("x86")) }
            if ($x64) { [void]($mergedFilterList.Add("x64")) }
            if ($ARM64) { [void]($mergedFilterList.Add("ARM64")) }
            if ($Filter) { $mergedFilterList.AddRange($Filter) }

            if ($mergedFilterList) {
                $filtersUsed = " using the filters: $( [string]::Join(", ", $mergedFilterList) )"
            } else {
                $filtersUsed = ""
            }

            foreach ($kbIterator in $KB) {
                try {
                    Write-Verbose "Searching the Microsoft Update Catalog for the UpdateID of KB `"$kbIterator`"${filtersUsed}."
                    $updateIdFromKb = GetUpdateIDFromKB -KB $kbIterator -Filter $mergedFilterList.ToArray() -ErrorAction Stop
                    [void]$targetUpdateIds.Add($updateIdFromKb)
                    $updateIdKbMap[$updateIdFromKb] = "$kbIterator"
                    Write-Verbose "KB $kbIterator resolved to UpdateID $updateIdFromKb."
                } catch {
                    if ($_.Exception -is [System.Net.WebException]) {
                        Write-Warning "Encountered a web exception while trying searching for the UpdateID of KB `"$kbIterator`": ${_}${warnSecurityProtocolAddendum}"
                    } elseif ($_.FullyQualifiedErrorId -eq "MultipleUpdatesFound,GetUpdateIDFromKB") {
                        Write-Warning "Found multiple updates while searching for KB $kbIterator."
                        Write-Warning "Please use the -Filter parameter to narrow your search, or use the update's UpdateID with the -UpdateID parameter of the cmdlet."
                    } else {
                        Write-Warning "Couldn't find UpdateID for KB $_ Are you using the right filters?"
                    }

                    Write-Error -ErrorRecord $_
                }
            }
        } elseif ($PSBoundParameters.ContainsKey("Uri")) {
            if ($Uri -notmatch ".*updateid=([a-zA-Z\d]{8}-(?:[a-zA-Z\d]{4}-){3}[a-zA-Z\d]{12}).*") {
                throw "-Uri parameter didn't match the expected format and the UpdateID could not be resolved. Please find the UpdateID manually, or use the -KB switch."
            }

            [void]$targetUpdateIds.Add($Matches[1])
        } else {
            [void]$targetUpdateIds.Add($UpdateID)
        }

        if (!$targetUpdateIds) {
            Write-Verbose "Aborting import since we could not resolve any UpdateIDs."
            return
        }

        $importingMultipleUpdates = $targetUpdateIds.Count -gt 1

        if (!$MetadataOnly) {
            Write-Output "Getting information about the update$(if ($importingMultipleUpdates) { "s" }) we're going to import from the Microsoft Update Catalog..."
        }

        foreach ($targetUpdateId in $targetUpdateIds) {
            $updateFilePaths = New-Object -TypeName "System.Collections.Generic.List[string]"

            if (!$MetadataOnly) {
                if ($PSBoundParameters.ContainsKey("Uri")) {
                    $targetUri = $Uri
                } else {
                    $targetUri = "https://www.catalog.update.microsoft.com/ScopedViewInline.aspx?updateid=$targetUpdateId"
                }

                try {
                    $updateInfo = GetMUCatalogUpdateInfo -Uri $targetUri -ErrorAction Stop
                } catch {
                    $addendum = if ($_.Exception -is [System.Net.WebException]) { $warnSecurityProtocolAddendum } else { "" }
                    Write-Error -Message "Failed to retrieve update information for update with ID ${targetUpdateId}: ${_}${addendum}" `
                        -Exception $_.Exception `
                        -Category InvalidResult `
                        -TargetObject $targetUri `
                        -ErrorId "CouldNotGetMUCatalogUpdateInfo"
                    continue
                }

                $targetUpdateId = $updateInfo["UpdateID"]
                $parsedFileInfoList = $updateInfo["Files"]
                $totalUpdateFiles = $parsedFileInfoList.Count
                Write-Output "Found $totalUpdateFiles file$( if ($totalUpdateFiles -gt 1) { "s" } ) to download."
                Write-Output "Checking to ensure all files are available for download..."

                # Check to ensure that all update files are available before attempting to download them.
                foreach ($parsedFileInfo in $parsedFileInfoList) {
                    $fileDownloadUri = $parsedFileInfo["Uri"]
                    $fileName = $parsedFileInfo["FileName"]
                    Write-Verbose "Checking to see if update file $fileName at $fileDownloadUri is available..."
                    $available, $responseErrorMessage, $responseError = TestDownloadAvailable -Uri $fileDownloadUri
                    $addendum = if ($responseError -is [System.Net.WebException]) { $warnSecurityProtocolAddendum } else { "" }

                    if (!$available) {
                        Write-Error -Message "File $fileName (URI $fileDownloadUri) was not available for download: $responseErrorMessage${addendum}" `
                            -Category "ResourceUnavailable" `
                            -ErrorId "UpdateFileDownloadNotAvailable" `
                            -TargetObject $fileDownloadUri
                        continue
                    }
                }

                try {
                    Write-Verbose "Downloading update files..."
                    $downloadFilesParams = @{
                        "FileInfoList" = $parsedFileInfoList
                        "OutputPath" = $DownloadPath
                    }

                    if ($DownloadInParallel) {
                        DownloadFilesInParallel @downloadFilesParams -ErrorAction Stop
                    } else {
                        DownloadFilesSequentially @downloadFilesParams -ErrorAction Stop
                    }
                } catch {
                    $addendum = if ($_.Exception -is [System.Net.WebException]) { $warnSecurityProtocolAddendum } else { "" }
                    Write-Error -Message "Failed downloading update files with the following error: ${_}${addendum}" `
                        -ErrorId "UpdateFileDownloadNotAvailable" `
                        -TargetObject $fileDownloadUri
                    continue
                }

                foreach ($parsedFileInfo in $parsedFileInfoList) {
                    $fileDownloadPath = Join-Path $DownloadPath $parsedFileInfo["FileName"] -ErrorAction Stop
                    [void]($updateFilePaths.Add($fileDownloadPath))
                }
            }

            try {
                $importedFiles = $false

                if ($MetadataOnly) {
                    if ($null -ne $updateIdKbMap[$targetUpdateId]) {
                        Write-Output "Importing metadata for $($updateIdKbMap[$targetUpdateId]) with update ID $targetUpdateId."
                    } else {
                        Write-Output "Importing metadata for update with ID $targetUpdateId."
                    }
                } else {
                    Write-Output "Importing downloaded update with ID $targetUpdateId into the local WSUS server."
                }

                $wsus.ImportUpdateFromCatalogSite($targetUpdateId, $updateFilePaths.ToArray())
                Write-Output "Successfully imported update $targetUpdateId into the local WSUS server."
                $importedFiles = $true
            } catch {
                $addendum = if ($_.Exception -is [System.Net.WebException]) { $warnSecurityProtocolAddendum } else { "" }

                if ($importingMultipleUpdates) {
                    Write-Error -Message "Failed importing update with ID ${targetUpdateId}: ${_}${addendum}" `
                        -Exception $_.Exception `
                        -ErrorId "FailedUpdateImport" `
                        -TargetObject $targetUpdateId
                } else {
                    Write-Warning "Failed importing the update into the local WSUS server."
                    Write-Warning "Please note that due to WSUS importing update metadata from the upstream server, the import will likely fail on disconnected systems syncing from Microsoft Update."
                    Write-Warning "The error was:"
                    $_ | Out-String | Write-Error

                    if (![string]::IsNullOrWhiteSpace($addendum)) {
                        Write-Warning "$addendum".TrimStart()
                    }

                    $confirmation = Read-Host -Prompt "Would you like to open the SoftwareDistribution.log file for manual error checking? (Y/n)"

                    if ([string]::IsNullOrWhiteSpace($confirmation) -or $confirmation.Trim() -eq "y") {
                        Start-Process -FilePath "${env:ProgramFiles}\Update Services\LogFiles\SoftwareDistribution.log" -ErrorAction Continue
                    }
                }
            } finally {
                if (!$MetadataOnly -and $removeFilesAfterImport) {
                    if ($importedFiles) {
                        # TODO: CTRL-C abort Write-Output seems wack, Write-Host instead?
                        Write-Output "Removing downloaded update files in $DownloadPath since they have been copied to the WSUSContent folder..."
                        $updateFilePaths | ForEach-Object {
                            Write-Verbose "Deleting file $_..."
                            Remove-Item -Path $_ -ErrorAction Continue
                        }
                    } else {
                        Write-Warning "The downloaded update files will remain in the download folder $DownloadPath until they are successfully imported or manually removed."
                    }
                }
            }
        }
    } finally {
        if ($HelpMe) {
            Write-Output ""
            Write-Output "============================="
            Write-Output "Is this a 64 Bit Process? $([Environment]::Is64BitProcess)"
            Write-Output "============================="

            Write-Output ""
            Write-Output "============================="
            Write-Output "Operating System Information"
            Write-Output "============================="
            $CimInstanceOperatingSystem = (Get-CimInstance Win32_OperatingSystem -Verbose:$false) | Select-Object *
            ($CimInstanceOperatingSystem | Out-String).Trim()

            Write-Output ""
            Write-Output "============================="
            Write-Output "Computer System Information"
            Write-Output "============================="
            $CimInstanceComputerSystem = (Get-CimInstance Win32_ComputerSystem -Verbose:$false) | Select-Object *
            ($CimInstanceComputerSystem | Out-String).Trim()

            Write-Output ""
            Write-Output "============================="
            Write-Output "WSUS Configuration"
            Write-Output "============================="

            if ($null -ne $wsusConfig) {
                $wsusConfig | Select-Object * -ExcludeProperty ProxyPassword | Format-List -Force | Out-String -Stream | Sort-Object | Where-Object { ![string]::IsNullOrWhiteSpace($_) }
            } else {
                Write-Output "Not available (WSUS configuration wasn't retrieved)."
            }

            Write-Output ""
            Write-Output "============================="
            Write-Output "Proxy Information"
            Write-Output "============================="
            Write-Output "Proxy: $(netsh winhttp show proxy)"
            $proxyEnable = Get-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings' -Name 'ProxyEnable' -ErrorAction SilentlyContinue

            if ($proxyEnable) {
                if (($proxyEnable | Select-Object -ExpandProperty 'ProxyEnable') -eq '1') {
                    try {
                        Get-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings' -Name 'ProxyOverride' -ErrorAction SilentlyContinue | Select-Object -ExpandProperty 'ProxyOverride'
                    } catch {
                        Write-Output "Bypass proxy server for local addresses is turned off."
                    }
                } else {
                    Write-Output "A proxy server has not been enabled on this user."
                }
            } else {
                Write-Output "ProxyEnable registry key didn't exist. A proxy server has not been enabled on this user."
            }

            Stop-Transcript -ErrorAction Continue
        }
    }
}

# Copyright (c) 2023-2024 AJ Tek Corporation. All Rights Reserved.

# SIG # Begin signature block
# MIIVmwYJKoZIhvcNAQcCoIIVjDCCFYgCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUzpjOk1xA3yhGmaSSs34QoWX/
# 3iWgghH7MIIFbzCCBFegAwIBAgIQSPyTtGBVlI02p8mKidaUFjANBgkqhkiG9w0B
# AQwFADB7MQswCQYDVQQGEwJHQjEbMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVy
# MRAwDgYDVQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEh
# MB8GA1UEAwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTIxMDUyNTAwMDAw
# MFoXDTI4MTIzMTIzNTk1OVowVjELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1NlY3Rp
# Z28gTGltaXRlZDEtMCsGA1UEAxMkU2VjdGlnbyBQdWJsaWMgQ29kZSBTaWduaW5n
# IFJvb3QgUjQ2MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjeeUEiIE
# JHQu/xYjApKKtq42haxH1CORKz7cfeIxoFFvrISR41KKteKW3tCHYySJiv/vEpM7
# fbu2ir29BX8nm2tl06UMabG8STma8W1uquSggyfamg0rUOlLW7O4ZDakfko9qXGr
# YbNzszwLDO/bM1flvjQ345cbXf0fEj2CA3bm+z9m0pQxafptszSswXp43JJQ8mTH
# qi0Eq8Nq6uAvp6fcbtfo/9ohq0C/ue4NnsbZnpnvxt4fqQx2sycgoda6/YDnAdLv
# 64IplXCN/7sVz/7RDzaiLk8ykHRGa0c1E3cFM09jLrgt4b9lpwRrGNhx+swI8m2J
# mRCxrds+LOSqGLDGBwF1Z95t6WNjHjZ/aYm+qkU+blpfj6Fby50whjDoA7NAxg0P
# OM1nqFOI+rgwZfpvx+cdsYN0aT6sxGg7seZnM5q2COCABUhA7vaCZEao9XOwBpXy
# bGWfv1VbHJxXGsd4RnxwqpQbghesh+m2yQ6BHEDWFhcp/FycGCvqRfXvvdVnTyhe
# Be6QTHrnxvTQ/PrNPjJGEyA2igTqt6oHRpwNkzoJZplYXCmjuQymMDg80EY2NXyc
# uu7D1fkKdvp+BRtAypI16dV60bV/AK6pkKrFfwGcELEW/MxuGNxvYv6mUKe4e7id
# FT/+IAx1yCJaE5UZkADpGtXChvHjjuxf9OUCAwEAAaOCARIwggEOMB8GA1UdIwQY
# MBaAFKARCiM+lvEH7OKvKe+CpX/QMKS0MB0GA1UdDgQWBBQy65Ka/zWWSC8oQEJw
# IDaRXBeF5jAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zATBgNVHSUE
# DDAKBggrBgEFBQcDAzAbBgNVHSAEFDASMAYGBFUdIAAwCAYGZ4EMAQQBMEMGA1Ud
# HwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29tL0FBQUNlcnRpZmlj
# YXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEFBQcwAYYYaHR0
# cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBDAUAA4IBAQASv6Hvi3Sa
# mES4aUa1qyQKDKSKZ7g6gb9Fin1SB6iNH04hhTmja14tIIa/ELiueTtTzbT72ES+
# BtlcY2fUQBaHRIZyKtYyFfUSg8L54V0RQGf2QidyxSPiAjgaTCDi2wH3zUZPJqJ8
# ZsBRNraJAlTH/Fj7bADu/pimLpWhDFMpH2/YGaZPnvesCepdgsaLr4CnvYFIUoQx
# 2jLsFeSmTD1sOXPUC4U5IOCFGmjhp0g4qdE2JXfBjRkWxYhMZn0vY86Y6GnfrDyo
# XZ3JHFuu2PMvdM+4fvbXg50RlmKarkUT2n/cR/vfw1Kf5gZV6Z2M8jpiUbzsJA8p
# 1FiAhORFe1rYMIIGGjCCBAKgAwIBAgIQYh1tDFIBnjuQeRUgiSEcCjANBgkqhkiG
# 9w0BAQwFADBWMQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVk
# MS0wKwYDVQQDEyRTZWN0aWdvIFB1YmxpYyBDb2RlIFNpZ25pbmcgUm9vdCBSNDYw
# HhcNMjEwMzIyMDAwMDAwWhcNMzYwMzIxMjM1OTU5WjBUMQswCQYDVQQGEwJHQjEY
# MBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMSswKQYDVQQDEyJTZWN0aWdvIFB1Ymxp
# YyBDb2RlIFNpZ25pbmcgQ0EgUjM2MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB
# igKCAYEAmyudU/o1P45gBkNqwM/1f/bIU1MYyM7TbH78WAeVF3llMwsRHgBGRmxD
# eEDIArCS2VCoVk4Y/8j6stIkmYV5Gej4NgNjVQ4BYoDjGMwdjioXan1hlaGFt4Wk
# 9vT0k2oWJMJjL9G//N523hAm4jF4UjrW2pvv9+hdPX8tbbAfI3v0VdJiJPFy/7Xw
# iunD7mBxNtecM6ytIdUlh08T2z7mJEXZD9OWcJkZk5wDuf2q52PN43jc4T9OkoXZ
# 0arWZVeffvMr/iiIROSCzKoDmWABDRzV/UiQ5vqsaeFaqQdzFf4ed8peNWh1OaZX
# nYvZQgWx/SXiJDRSAolRzZEZquE6cbcH747FHncs/Kzcn0Ccv2jrOW+LPmnOyB+t
# AfiWu01TPhCr9VrkxsHC5qFNxaThTG5j4/Kc+ODD2dX/fmBECELcvzUHf9shoFvr
# n35XGf2RPaNTO2uSZ6n9otv7jElspkfK9qEATHZcodp+R4q2OIypxR//YEb3fkDn
# 3UayWW9bAgMBAAGjggFkMIIBYDAfBgNVHSMEGDAWgBQy65Ka/zWWSC8oQEJwIDaR
# XBeF5jAdBgNVHQ4EFgQUDyrLIIcouOxvSK4rVKYpqhekzQwwDgYDVR0PAQH/BAQD
# AgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAwEwYDVR0lBAwwCgYIKwYBBQUHAwMwGwYD
# VR0gBBQwEjAGBgRVHSAAMAgGBmeBDAEEATBLBgNVHR8ERDBCMECgPqA8hjpodHRw
# Oi8vY3JsLnNlY3RpZ28uY29tL1NlY3RpZ29QdWJsaWNDb2RlU2lnbmluZ1Jvb3RS
# NDYuY3JsMHsGCCsGAQUFBwEBBG8wbTBGBggrBgEFBQcwAoY6aHR0cDovL2NydC5z
# ZWN0aWdvLmNvbS9TZWN0aWdvUHVibGljQ29kZVNpZ25pbmdSb290UjQ2LnA3YzAj
# BggrBgEFBQcwAYYXaHR0cDovL29jc3Auc2VjdGlnby5jb20wDQYJKoZIhvcNAQEM
# BQADggIBAAb/guF3YzZue6EVIJsT/wT+mHVEYcNWlXHRkT+FoetAQLHI1uBy/YXK
# ZDk8+Y1LoNqHrp22AKMGxQtgCivnDHFyAQ9GXTmlk7MjcgQbDCx6mn7yIawsppWk
# vfPkKaAQsiqaT9DnMWBHVNIabGqgQSGTrQWo43MOfsPynhbz2Hyxf5XWKZpRvr3d
# MapandPfYgoZ8iDL2OR3sYztgJrbG6VZ9DoTXFm1g0Rf97Aaen1l4c+w3DC+IkwF
# kvjFV3jS49ZSc4lShKK6BrPTJYs4NG1DGzmpToTnwoqZ8fAmi2XlZnuchC4NPSZa
# PATHvNIzt+z1PHo35D/f7j2pO1S8BCysQDHCbM5Mnomnq5aYcKCsdbh0czchOm8b
# kinLrYrKpii+Tk7pwL7TjRKLXkomm5D1Umds++pip8wH2cQpf93at3VDcOK4N7Ew
# oIJB0kak6pSzEu4I64U6gZs7tS/dGNSljf2OSSnRr7KWzq03zl8l75jy+hOds9TW
# SenLbjBQUGR96cFr6lEUfAIEHVC1L68Y1GGxx4/eRI82ut83axHMViw1+sVpbPxg
# 51Tbnio1lB93079WPFnYaOvfGAA0e0zcfF/M9gXr+korwQTh2Prqooq2bYNMvUoU
# KD85gnJ+t0smrWrb8dee2CvYZXD5laGtaAxOfy/VKNmwuWuAh9kcMIIGZjCCBM6g
# AwIBAgIRAMyLU7NDPs1zQXTEeYo/k2YwDQYJKoZIhvcNAQEMBQAwVDELMAkGA1UE
# BhMCR0IxGDAWBgNVBAoTD1NlY3RpZ28gTGltaXRlZDErMCkGA1UEAxMiU2VjdGln
# byBQdWJsaWMgQ29kZSBTaWduaW5nIENBIFIzNjAeFw0yMjA2MzAwMDAwMDBaFw0y
# NTA2MjkyMzU5NTlaMFkxCzAJBgNVBAYTAkNBMRAwDgYDVQQIDAdPbnRhcmlvMRsw
# GQYDVQQKDBJBSiBUZWsgQ29ycG9yYXRpb24xGzAZBgNVBAMMEkFKIFRlayBDb3Jw
# b3JhdGlvbjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAOLV0fXklSI7
# zs4Awbc5rzQGG/XJaEgZLEXFVvzqKWlApLYSdXMLTzW14/IEoVMmi+qunmgIF98c
# 3gUFm6dTSkf+ccSIWgHwjD/pPMX4leIO1Qen+OBup4p33XRNqHsWLzBqlWmKRcLW
# 5cyRDRXedgZDvZozhxTqLhk0tFdTcBoGULA13Z/TfotbwNCx05W304VgluUogNX2
# 4yv80QYmHa6667ewXuAPNiPWQgFE4fyXOvFsRUFEtllRZmUpOPqZtGURw2ZYd+cZ
# UFn4QrC9DzlESFEjQzMMF5iUjrKCI25jq53MkTZHnOjO9KGH5SvcLo5CbgNptpzr
# d1P7AsoHFedvErNbp6YOhKrkv7F0ksVgqBh1v2Yc1ShpzZBEJ6pL+QsdnWzl6Wzb
# B8xEYEdbNF0A0/kyFHm2tYSxxCsVO4caoL2CcsA3q/1h73MBWEcNT2fV6o25L+LY
# sNUkFRc6ZRqOQ/ub9na8jCOwuhxi8MXTGo22Crinp5bbzYA/liNm97G77mJe59za
# aaoulTWzk5y06k7VgLDYtR7IgZ47Aytnj/jr7S5/P/1F2K3lbRUdGyKdifubSgjk
# m1gubK0cm3Y6L7Ar2X1nBm+PpAZqA54jQBs9cV03MVDjZb3AvjooCi9uOCJ4998U
# TV9Sn2873/GUXE6dRB+w0fD/FcUxX2pjAgMBAAGjggGsMIIBqDAfBgNVHSMEGDAW
# gBQPKssghyi47G9IritUpimqF6TNDDAdBgNVHQ4EFgQU0JE9KqcvG1A1pTeA+vcd
# t1flqK0wDgYDVR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwEwYDVR0lBAwwCgYI
# KwYBBQUHAwMwSgYDVR0gBEMwQTA1BgwrBgEEAbIxAQIBAwIwJTAjBggrBgEFBQcC
# ARYXaHR0cHM6Ly9zZWN0aWdvLmNvbS9DUFMwCAYGZ4EMAQQBMEkGA1UdHwRCMEAw
# PqA8oDqGOGh0dHA6Ly9jcmwuc2VjdGlnby5jb20vU2VjdGlnb1B1YmxpY0NvZGVT
# aWduaW5nQ0FSMzYuY3JsMHkGCCsGAQUFBwEBBG0wazBEBggrBgEFBQcwAoY4aHR0
# cDovL2NydC5zZWN0aWdvLmNvbS9TZWN0aWdvUHVibGljQ29kZVNpZ25pbmdDQVIz
# Ni5jcnQwIwYIKwYBBQUHMAGGF2h0dHA6Ly9vY3NwLnNlY3RpZ28uY29tMCEGA1Ud
# EQQaMBiBFmFqLmFjY291bnRpbmdAYWp0ZWsuY2EwDQYJKoZIhvcNAQEMBQADggGB
# ACa/QJzMPVDohIgQtL4/Yr7z7DlaybhjLQ2gEx7pU6G/hNQ+kClqLR3hykkJ7fwZ
# 4cYX1TF4JWnkmQ0GZwj5bk5RaSyDatQVVIW8AQNMIEEhUftHJn1REz16CzXxlgzd
# d+GTqYmwommBa6DlFO88fwF0FL3KJgvNguQGSU9sGIGUWyQuxqFUwXRgsXpwQbpR
# 1H6qsViLN1SGPXO+iqUSyejmd9mIbc8b0IxuR6rDtU2PIcU8XzwsJv8M/L1paQut
# 4m7dNw54gsoRVp+KJWawkkEVM6xvszCo8VIk8ZGetRCBT+ZSunIb/LFripb++lR8
# tIBkq8zYEhEp9U8GQZbKNZzfelix4kRt5wUq39rpBU8aHoU4GRXFs571jb/qBz/x
# AVckN5cosppJxe+AW/TR9qKrL8uKOJ9cCLRXjPdLGSmHA5XMN6ecgRE+yfLMOauX
# pUo33dCGh36TZuSi7P4uz2trEEUfmaQlr/TEp0xtbgUxTopdUYh9xagN4bbkRmoB
# izGCAwowggMGAgEBMGkwVDELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1NlY3RpZ28g
# TGltaXRlZDErMCkGA1UEAxMiU2VjdGlnbyBQdWJsaWMgQ29kZSBTaWduaW5nIENB
# IFIzNgIRAMyLU7NDPs1zQXTEeYo/k2YwCQYFKw4DAhoFAKB4MBgGCisGAQQBgjcC
# AQwxCjAIoAKAAKECgAAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYB
# BAGCNwIBCzEOMAwGCisGAQQBgjcCARUwIwYJKoZIhvcNAQkEMRYEFILK0gLwRoG6
# YWlMskyqEuAqY5VlMA0GCSqGSIb3DQEBAQUABIICAHe09Fz28Nzl/gYII2TmCMp6
# zEYcAgHaBlItNenG3sGlXXfu+Y31GCD5jV95fYMZXo3mctZx7SbIsGU0jxdZ+adM
# yZwQ2Ct4OQnl6p3NQjdFEz4bPxwCPnS21MZXibKT5Su6ctgxJVCW+T+VArwWz943
# VlgJp0fY+/BWx6o6qZlud8ztgYhjDcP/B+fygbCFQceBrhJ8DvP/qUOQLMg0VeDF
# SE8fHLPhCxJG2L15l47r60nZHekYXsDDbeix8RXZY6VJuCLtU2+J4aUtnzHBkQKY
# Pt3KtHR0oK2BAMkcSh+0N6bpAp6e22paU2xcSXL5w0eHuEAMr1m6N75vuOT7Iluo
# zDfu+TUqNt1vpAJRJq1Nx/x0LtHsG2zMaRZngPYvk2r5zQfkXEqkUXQaLhi5k9NM
# gc3J17og5jhL8kLWo5BWLEMkBg6D2W1Rli7XBF0hSVUni22oWrvIZR6h9KV3f+k/
# xLJDlyFbSgqpwtpN5KvnBLxagIpdBkUthL4byeWpTOFS5FWTsjtsCxgebvGyF+RI
# TGwcFDGq4vkDsBrXz2T8K+AdZ3omCGoh1vCnRHPUrxzhARmlIfkIkvlWNtG+wtkY
# kmFErSUicMiZkPYSEbdILmRrNwCAjP65bf9DDGGTrU2x04PuKxogCeHY3TkIsTq9
# yqsODQWCcVM0LdjFa6Ul
# SIG # End signature block