WinUpd.psm1

<#
#>


########
# Global settings
$ErrorActionPreference = "Stop"
$InformationPreference = "Continue"
Set-StrictMode -Version 2

Function Update-WinUpdCabFile
{
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$Path,

        [Parameter(Mandatory=$false)]
        [ValidateNotNullOrEmpty()]
        [string]$Uri = "https://catalog.s.download.windowsupdate.com/microsoftupdate/v6/wsusscan/wsusscn2.cab",

        [Parameter(Mandatory=$false)]
        [ValidateNotNull()]
        [switch]$Force = $false
    )

    process
    {
        # Modification times for comparison
        $cabModificationTime = $null
        $urlModificationTime = $null

        # Determine modification time for the local cab file
        try {
            Write-Verbose "Getting current cab file modification time"
            $cabModificationTime = (Get-Item $Path).LastWriteTimeUtc

            Write-Verbose "Cab Modification Time: $cabModificationTime"
        } catch {
            Write-Verbose "Could not get current cab file modification time: $_"
        }

        # Determine modification time for the remote cab file
        Write-Verbose "Retrieving URL modification time"
        $params = @{
            UseBasicParsing = $true
            Method = "Head"
            Uri = $Uri
        }

        $headers = Invoke-WebRequest @params

        # Extract modification time
        try {
            $urlModificationTime = [DateTime]::Parse($headers.Headers["Last-Modified"])

            Write-Verbose "Url Modification Time: $urlModificationTime"
        } catch {
            Write-Warning "Failed to parse Last-Modified header as DateTime: $_"
        }

        # If we have cab and url modification times and the local file is newer than the url, then finish here
        if (!$Force -and $null -ne $cabModificationTime -and $null -ne $urlModificationTime -and $cabModificationTime -gt $urlModificationTime)
        {
            Write-Verbose "Local file is newer than the Uri. Not downloading."
            return
        }

        Write-Verbose "wsusscn2.cab file needs updating. Downloading."

        # Temporary path for download file
        $tempPath = $Path + ".tmp"
        if (Test-Path $tempPath)
        {
            Remove-Item -Force $tempPath
        }

        # Download to temporary location
        $params = @{
            UseBasicParsing = $true
            Method = "Get"
            Uri = $Uri
            OutFile = $tempPath
        }

        Invoke-WebRequest @params

        # Move the temporary location to the actual place for the scn2 cab file
        Move-Item $tempPath $Path -Force

        Write-Verbose "Successfully updated wsusscn2.cab"
    }
}

Function Remove-WinUpdOfflineScan
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$false)]
        [ValidateNotNullOrEmpty()]
        [string]$Name = "Offline Sync Service"
    )

    Process
    {
        # Create the ServiceManager object
        Write-Verbose "Creating ServiceManager object"
        $manager = New-Object -ComObject Microsoft.Update.ServiceManager

        # Remove any services by this name
        $services = $manager.Services | Where-Object { $_.Name -eq $Name } | ForEach-Object { $_ }
        $services | ForEach-Object {
            $service = $_

            Write-Verbose ("Removing service with ID: " + $service.ServiceID)
            $manager.RemoveService($service.ServiceID)
        }
    }
}

Function Update-WinUpdOfflineScan
{
    param(
        [Parameter(Mandatory=$false)]
        [ValidateNotNullOrEmpty()]
        [string]$OfflineServiceName = "Offline Sync Service",

        [Parameter(Mandatory=$false)]
        [ValidateNotNullOrEmpty()]
        [string]$CabFile = $null,

        [Parameter(Mandatory=$false)]
        [ValidateNotNull()]
        [switch]$Force = $false
    )

    process
    {
        # Create the service manager to interact with Windows Update
        $manager = New-Object -ComObject Microsoft.Update.ServiceManager

        # Collect all package services matching the name
        [array]$services = @($manager.Services | ForEach-Object { $_ })
        $services = $services |
            Where-Object { $_.Name -eq $OfflineServiceName } |
            Sort-Object -Property IssueDate -Descending
        Write-Verbose "Current package services:"
        Write-Verbose ($services | Format-Table -Property Name,ServiceId,IssueDate | Out-String)

        # Remove all but the newest
        $services | Select-Object -Skip 1 | ForEach-Object {
            $service = $_

            Write-Verbose ("Removing service with ID: " + $service.ServiceID)
            $manager.RemoveService($service.ServiceID)
        }

        # Get details for the active service, if there is one
        $currentIssueDate = $null
        $currentServiceId = $null
        if (($services | Measure-Object).Count -gt 0)
        {
            #$currentIssueDate = [DateTime]::SpecifyKind($services[0].IssueDate, [DateTimeKind]::Utc)
            $currentIssueDate = $services[0].IssueDate
            $currentServiceId = $services[0].ServiceID

            Write-Verbose ("Current service issue date: " + $currentIssueDate.ToString("o"))
            Write-Verbose ("Current service ID: " + $currentServiceId)
        }

        # Get the full path to the cab file
        $CabFile = (Get-Item $CabFile).FullName
        Write-Verbose "Cab File path: $CabFile"

        # Get the cab file modification time
        # Note - IssueDate is in UTC
        $cabModificationTime = (Get-Item $CabFile).LastWriteTimeUtc
        Write-Verbose ("Cab modification time: " + $cabModificationTime.ToString("o"))

        # Add the offline scan file to Windows Update
        if ($Force -or $null -eq $currentIssueDate -or $cabModificationTime -gt $currentIssueDate)
        {
            Write-Verbose "Updating searcher to use the local cab file"

            # Add the cab file to Windows Update
            $service = $manager.AddScanPackageService($OfflineServiceName, $CabFile, 1)

            if ($null -ne $currentServiceId)
            {
                # Remove the now defunct service
                Write-Verbose "Removing defunct service: $currentServiceId"
                $manager.RemoveService($currentServiceId)
            }

            # Return the new service ID
            Write-Verbose ("New service ID: {0}" -f $service.ServiceId)
            $service.ServiceId
        } else {
            Write-Verbose "Service does not need updating. Returning current service: $currentServiceId"

            # Return the current service ID
            $currentServiceId
        }
    }
}

Function Get-WinUpdScanServices
{
    [CmdletBinding()]
    param()

    Process
    {
        # Create the ServiceManager object
        Write-Verbose "Creating ServiceManager object"
        $manager = New-Object -ComObject Microsoft.Update.ServiceManager

        $manager.Services
    }
}

Function Get-WinUpdUpdates
{
    param(
        [Parameter(Mandatory=$false)]
        [ValidateNotNullOrEmpty()]
        [string]$ServiceId = $null
    )

    process
    {
        # Create the ServiceManager object
        Write-Verbose "Creating ServiceManager object"
        $manager = New-Object -ComObject Microsoft.Update.ServiceManager

        # Create an update searcher
        Write-Verbose "Creating update session using new scan service"
        $session = New-Object -ComObject Microsoft.Update.session
        $searcher = $session.CreateUpdateSearcher()

        # Use a scan service, if specified
        if (![string]::IsNullOrEmpty($ServiceId))
        {
            # Update the searcher to use the cab file
            Write-Verbose "Updating searcher to use the local cab file"
            $searcher.ServerSelection = 3
            $searcher.ServiceID = $ServiceId
        }

        # Capture anything not installed
        Write-Verbose "Searching for packages that are not installed"
        $result = $searcher.Search("IsInstalled=0")
        $count = ($result.Updates | Measure-Object).Count
        Write-Verbose "Found $count updates not installed"

        # Report on any updates found
        $result.Updates
    }
}

Function Install-WinUpdUpdates
{
    param(
        [Parameter(Mandatory=$true)]
        $Updates,

        [Parameter(Mandatory=$false)]
        [ValidateNotNull()]
        [switch]$DownloadOnly = $false
    )

    process
    {
        # Create a new Update Collection
        $updateCol = New-Object -ComObject Microsoft.Update.UpdateColl
        $Updates | ForEach-Object { $updateCol.Add($_) | Out-Null }

        # Report on applicable updates
        $count = ($updateCol | Measure-Object).Count
        Write-Verbose "$count updates to apply"

        # Quit here if there are no updates to apply
        if (($updateCol | Measure-Object).Count -eq 0)
        {
            return
        }

        # Download any packages
        Write-Verbose "Starting download of updates"
        $downloader = New-Object -ComObject Microsoft.Update.Downloader
        $downloader.Updates = $updateCol
        $downloader.Download() | Out-Null

        if (!$DownloadOnly)
        {
            # Install any packages
            Write-Verbose "Starting installation of updates"
            $installer = New-Object -ComObject Microsoft.Update.Installer
            $installer.ForceQuiet = $true
            $installer.Updates = $updateCol
            $installer.Install() | Out-Null

            Write-Verbose "Update installation completed"
        }
    }
}

Function Get-WinUpdRebootRequired
{
    param()

    process
    {
        $systemInfo = New-Object -ComObject Microsoft.Update.SystemInfo
        [bool]($systemInfo.RebootRequired)
    }
}