library/xWindowsUpdate/2.8.0.0/DscResources/MSFT_xWindowsUpdateAgent/MSFT_xWindowsUpdateAgent.psm1

Set-StrictMode -Version latest
$script:WuaSearchString = 'IsAssigned=1 and IsHidden=0 and IsInstalled=0'
function Get-WuaServiceManager {
    return (New-Object -ComObject Microsoft.Update.ServiceManager)
}

function Add-WuaService {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $ServiceId,

        [int]
        $Flags = 7,

        [string]
        $AuthorizationCabPath = [string]::Empty

    )

    $wuaServiceManager = Get-WuaServiceManager
    $wuaServiceManager.AddService2($ServiceId, $Flags, $AuthorizationCabPath)
}

function Remove-WuaService {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $ServiceId
    )

    $wuaServiceManager = Get-WuaServiceManager
    $wuaServiceManager.RemoveService($ServiceId)
}

function Get-WuaSearchString {
    param(
        [switch]
        $security,
        [switch]
        $optional,
        [switch]
        $important
    )
    $securityCategoryId = "'0FA1201D-4330-4FA8-8AE9-B877473B6441'"
    <#
 
 
 
 
 
    # invalid, would not install anything - not security and not optional and not important
    #>

    # security and optional and important
    # not security and optional and important
    if ($optional -and $important) {
        # Installing everything not hidden and not already installed
        return 'IsHidden=0 and IsInstalled=0'
    }
    # security and optional and not important
    elseif ($security -and $optional) {
        # or can only be used at the top most boolean expression
        return "(IsAssigned=0 and IsHidden=0 and IsInstalled=0) or (CategoryIds contains $securityCategoryId and IsHidden=0 and IsInstalled=0)"
    }
    # security and not optional and important
    elseif ($security -and $important ) {
        # Installing everything not hidden,
        # not optional (optional are not assigned) and not already installed
        return 'IsAssigned=1 and IsHidden=0 and IsInstalled=0'
    } elseif ($optional -and $important) {
        # Installing everything not hidden,
        # not optional (optional are not assigned) and not already installed
        return 'IsHidden=0 and IsInstalled=0'

    }
    # security and not optional and not important
    elseif ($security) {
        # Installing everything that is security and not hidden,
        # and not already installed
        return "CategoryIds contains $securityCategoryId and IsHidden=0 and IsInstalled=0"
    }
    # not security and not optional and important
    elseif ($important) {
        # Installing everything that is not hidden,
        # is assigned (not optional) and not already installed
        # not valid cannot do not contains or a boolean not
        # Note important updates will include security updates
        return "IsAssigned=1 and IsHidden=0 and IsInstalled=0"
    }
    # not security and optional and not important
    elseif ($optional) {
        # Installing everything that is not hidden,
        # is not assigned (is optional) and not already installed
        # not valid cannot do not contains or a boolean not

        # Note optional updates may include security updates
        return "IsAssigned=0 and IsHidden=0 and IsInstalled=0"
    }

    return "CategoryIds contains $securityCategoryId and IsHidden=0 and IsInstalled=0"

}

function Get-WuaAu {
    return (New-Object -ComObject 'Microsoft.Update.AutoUpdate')
}

function Get-WuaAuSettings {
    return (Get-WuaAu).Settings
}

function Get-WuaWrapper {
    param(
        [ScriptBlock] $tryBlock,
        [object[]] $argumentList,
        [Parameter(ParameterSetName = 'OneValue')]
        [object] $ExceptionReturnValue = $null
    )

    try {
        return Invoke-Command -ScriptBlock $tryBlock -NoNewScope -ArgumentList $argumentList
    } catch [System.Runtime.InteropServices.COMException] {
        switch ($_.Exception.HResult) {
            # 0x8024001e -2145124322 WU_E_SERVICE_STOP Operation did not complete because the service or system was being shut down. wuerror.h
            -2145124322 {
                Write-Warning 'Got an error that WU service is stopping. Handling the error.'
                return $ExceptionReturnValue
            }
            # 0x8024402c -2145107924 WU_E_PT_WINHTTP_NAME_NOT_RESOLVED Same as ERROR_WINHTTP_NAME_NOT_RESOLVED - the proxy server or target server name cannot be resolved. wuerror.h
            -2145107924 {
                # TODO: add retry for this error
                Write-Warning 'Got an error that WU could not resolve the name of the update service. Handling the error.'
                return $ExceptionReturnValue
            }
            # 0x8024401c -2145107940 WU_E_PT_HTTP_STATUS_REQUEST_TIMEOUT Same as HTTP status 408 - the server timed out waiting for the request. wuerror.h
            -2145107940 {
                # TODO: add retry for this error
                Write-Warning 'Got an error a request timed out (http status 408 or equivalent) when WU was communicating with the update service. Handling the error.'
                return $ExceptionReturnValue
            }
            # 0x8024402f -2145107921 WU_E_PT_ECP_SUCCEEDED_WITH_ERRORS External cab file processing completed with some errors. wuerror.h
            -2145107921 {
                # No retry needed
                Write-Warning 'Got an error that CAB processing completed with some errors.'
                return $ExceptionReturnValue
            }
            default {
                throw
            }
        }
    }
}

function Get-WuaAuNotificationLevel {
    return Get-WuaWrapper -tryBlock {
        switch ((Get-WuaAuSettings).NotificationLevel) {
            0 { return 'Not Configured' }
            1 { return 'Disabled' }
            2 { return 'Notify before download' }
            3 { return 'Notify before installation' }
            4 { return 'Scheduled installation' }
            default { return 'Reserved' }
        }
    } -ExceptionReturnValue [string]::Empty
}

function Invoke-WuaDownloadUpdates {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [object] $UpdateCollection
    )

    $downloader = (Get-WuaSession).CreateUpdateDownloader()
    $downloader.Updates = $UpdateCollection

    Write-Verbose -Message 'Downloading updates...' -Verbose
    $downloadResult = $downloader.Download()
}

function Invoke-WuaInstallUpdates {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [object] $UpdateCollection
    )

    $installer = (Get-WuaSession).CreateUpdateInstaller()

    $installer.Updates = $UpdateCollection
    Write-Verbose -Message 'Installing updates...' -Verbose
    $installResult = $installer.Install()
}

function Set-WuaAuNotificationLevel {
    param(
        [ValidateSet('Not Configured', 'Disabled', 'Notify before download', 'Notify before installation', 'Scheduled installation', 'ScheduledInstallation')]
        [string]
        $notificationLevel
    )
    $intNotificationLevel = Get-WuaAuNotificationLevelInt -notificationLevel $notificationLevel

    $settings = Get-WuaAuSettings
    $settings.NotificationLevel = $intNotificationLevel
    $settings.Save()
}

function Get-WuaAuNotificationLevelInt {
    param(
        [ValidateSet('Not Configured', 'Disabled', 'Notify before download', 'Notify before installation', 'Scheduled installation', 'ScheduledInstallation')]
        [string]
        $notificationLevel
    )
    $intNotificationLevel = 0

    switch -Regex ($notificationLevel) {
        '^Not\s*Configured$' { $intNotificationLevel = 0 }
        '^Disabled$' { $intNotificationLevel = 1 }
        '^Notify\s*before\s*download$' { $intNotificationLevel = 2 }
        '^Notify\s*before\s*installation$' { $intNotificationLevel = 3 }
        '^Scheduled\s*installation$' { $intNotificationLevel = 4 }
        default { throw 'Invalid notification level' }
    }

    return $intNotificationLevel
}

function Get-WuaSystemInfo {
    return (New-Object -ComObject 'Microsoft.Update.SystemInfo')
}

function Get-WuaRebootRequired {
    return Get-WuaWrapper -tryBlock {
        Write-Verbose -Message 'TryGet RebootRequired...' -Verbose
        $rebootRequired = (Get-WuaSystemInfo).rebootRequired
        Write-Verbose -Message "Got rebootRequired: $rebootRequired" -Verbose
        return $rebootRequired
    } -ExceptionReturnValue $true

}
function Get-WuaSession {
    return (New-Object -ComObject 'Microsoft.Update.Session')
}

function get-WuaSearcher {
    [cmdletbinding(DefaultParameterSetName = 'category')]
    param(
        [parameter(Mandatory = $true, ParameterSetName = 'searchString')]
        [System.String]
        $SearchString,

        [parameter(ParameterSetName = 'category')]
        [ValidateSet("Security", "Important", "Optional")]
        [AllowEmptyCollection()]
        [AllowNull()]
        [System.String[]]
        $Category = @('Security')

    )

    $memberSearchString = $SearchString
    if ($PSCmdlet.ParameterSetName -eq 'category') {
        $searchStringParams = @{ }
        foreach ($CategoryItem in $Category) {
            $searchStringParams[$CategoryItem] = $true
        }
        $memberSearchString = (Get-WuaSearchString @searchStringParams)
    }

    return Get-WuaWrapper -tryBlock {
        param(
            [parameter(Mandatory = $true)]
            [System.String]
            $memberSearchString
        )
        Write-Verbose -Message "Searching for updating using: $memberSearchString" -Verbose
        return ((Get-WuaSession).CreateUpdateSearcher()).Search($memberSearchString)
    } -argumentList @($memberSearchString)
}

function Test-SearchResult {
    [CmdletBinding()]
    [OutputType([bool])]
    param
    (
        [parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [System.Object]
        $SearchResult
    )

    if (!(@($SearchResult | get-member | Select-Object -ExpandProperty Name) -contains 'Updates')) {
        Write-Verbose 'Did not find updates on SearchResult'
        return $false
    }
    if (!(@(Get-Member -InputObject $SearchResult.Updates | Select-Object -ExpandProperty Name) -contains 'Count')) {
        Write-Verbose 'Did not find count on updates on SearchResult'
        return $false
    }
    return $true
}

function Get-TargetResource {
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param
    (
        [parameter(Mandatory = $true)]
        [ValidateSet("Yes")]
        [System.String]
        $IsSingleInstance,

        [ValidateSet("Security", "Important", "Optional")]
        [AllowEmptyCollection()]
        [AllowNull()]
        [System.String[]]
        $Category = @('Security'),

        [ValidateSet("Disabled", "ScheduledInstallation")]
        [System.String]
        $Notifications,

        [parameter(Mandatory = $true)]
        [ValidateSet("WindowsUpdate", "MicrosoftUpdate", "WSUS")]
        [System.String]
        $Source,

        [bool]
        $UpdateNow = $false
    )

    Test-TargetResourceProperties @PSBoundParameters
    $totalUpdatesNotInstalled = $null
    $UpdateNowReturn = $null
    $rebootRequired = $null
    if ($UpdateNow) {
        $rebootRequired = Get-WuaRebootRequired
        $SearchResult = (get-WuaSearcher -Category $Category)
        $totalUpdatesNotInstalled = 0
        if ($SearchResult -and (Test-SearchResult -SearchResult $SearchResult)) {
            $totalUpdatesNotInstalled = $SearchResult.Updates.Count
        }
        $UpdateNowReturn = $false
        if ($totalUpdatesNotInstalled -eq 0 -and !$rebootRequired) {
            $UpdateNowReturn = $true
        }
    }

    $notificationLevel = (Get-WuaAuNotificationLevel)


    $CategoryReturn = $Category

    $SourceReturn = 'WindowsUpdate'
    $UpdateServices = (Get-WuaServiceManager).Services
    #Check if the microsoft update service is registered
    $defaultService = @($UpdateServices).where{ $_.IsDefaultAuService }
    Write-Verbose -Message "Get default search service: $($defaultService.ServiceId)"
    if ($defaultService.ServiceId -eq '7971f918-a847-4430-9279-4a52d1efe18d') {
        $SourceReturn = 'MicrosoftUpdate'
    } elseif ($defaultService.IsManaged) {
        $SourceReturn = 'WSUS'
    }

    $returnValue = @{
        IsSingleInstance                    = 'Yes'
        Category                            = $CategoryReturn
        AutomaticUpdatesNotificationSetting = $notificationLevel
        TotalUpdatesNotInstalled            = $totalUpdatesNotInstalled
        RebootRequired                      = $rebootRequired
        Notifications                       = $notificationLevel
        Source                              = $SourceReturn
        UpdateNow                           = $UpdateNowReturn
    }
    $returnValue
}


function Set-TargetResource {
    # should be [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "DSCMachineStatus")], but it doesn't work
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "")]
    [CmdletBinding(SupportsShouldProcess = $true)]
    param
    (
        [parameter(Mandatory = $true)]
        [ValidateSet("Yes")]
        [System.String]
        $IsSingleInstance,

        [ValidateSet("Security", "Important", "Optional")]
        [System.String[]]
        $Category = @('Security'),

        [ValidateSet("Disabled", "ScheduledInstallation")]
        [System.String]
        $Notifications,

        [parameter(Mandatory = $true)]
        [ValidateSet("WindowsUpdate", "MicrosoftUpdate", "WSUS")]
        [System.String]
        $Source,

        [bool]
        $UpdateNow = $false
    )

    Test-TargetResourceProperties @PSBoundParameters

    $Get = Get-TargetResource @PSBoundParameters

    $updateCompliant = ($UpdateNow -eq $false -or $Get.UpdateNow -eq $UpdateNow)
    Write-Verbose "updateNow compliant: $updateCompliant"
    $notificationCompliant = (!$Notifications -or $Notifications -eq $Get.Notifications)
    Write-Verbose "notifications compliant: $notificationCompliant"
    $SourceCompliant = (!$Source -or $Source -eq $Get.Source)
    Write-Verbose "service compliant: $SourceCompliant"

    If (!$updateCompliant -and $PSCmdlet.ShouldProcess("Install Updates")) {
        $SearchResult = (get-WuaSearcher -Category $Category)
        if ($SearchResult -and $SearchResult.Updates.Count -gt 0) {
            Write-Verbose -Verbose -Message 'Installing updates...'
            #Write Results
            foreach ($update in $SearchResult.Updates) {
                $title = $update.Title
                Write-Verbose -Message "Found update: $Title" -Verbose
            }

            Invoke-WuaDownloadUpdates -UpdateCollection $SearchResult.Updates

            Invoke-WuaInstallUpdates -UpdateCollection $SearchResult.Updates


        } else {
            Write-Verbose -Verbose -Message 'No updates'
        }
        Write-Verbose -Verbose -Message 'Checking for a reboot...'
        $rebootRequired = (Get-WuaRebootRequired)
        if ($rebootRequired) {
            Write-Verbose -Verbose -Message 'A reboot was required'
            $global:DSCMachineStatus = 1
        } else {
            Write-Verbose -Verbose -Message 'A reboot was NOT required'
        }
    }

    If (!$notificationCompliant -and $PSCmdlet.ShouldProcess("Set notifications to: $notifications")) {
        Try {
            #TODO verify that group policy is not overriding this settings
            # if it is throw an error, if it conflicts
            Set-WuaAuNotificationLevel -notificationLevel $Notifications
        } Catch {
            $ErrorMsg = $_.Exception.Message
            Write-Verbose $ErrorMsg
        }
    }

    If (!$SourceCompliant ) {
        if ($Source -eq 'MicrosoftUpdate' -and $PSCmdlet.ShouldProcess("Enable Microsoft Update Service")) {
            Write-Verbose "Enable the Microsoft Update setting"
            Add-WuaService -ServiceId '7971f918-a847-4430-9279-4a52d1efe18d'
            Restart-Service wuauserv -ErrorAction SilentlyContinue
        } elseif ($PSCmdlet.ShouldProcess("Disable Microsoft Update Service")) {
            Write-Verbose "Disable the Microsoft Update setting"
            Remove-WuaService -ServiceId '7971f918-a847-4430-9279-4a52d1efe18d'
        }

    }
}


function Test-TargetResource {
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param
    (
        [parameter(Mandatory = $true)]
        [ValidateSet("Yes")]
        [System.String]
        $IsSingleInstance,

        [ValidateSet("Security", "Important", "Optional")]
        [System.String[]]
        $Category = @('Security'),

        [ValidateSet("Disabled", "ScheduledInstallation")]
        [System.String]
        $Notifications,

        [parameter(Mandatory = $true)]
        [ValidateSet("WindowsUpdate", "MicrosoftUpdate", "WSUS")]
        [System.String]
        $Source,

        [bool]
        $UpdateNow = $false
    )

    Test-TargetResourceProperties @PSBoundParameters
    #Output the result of Get-TargetResource function.
    $Get = Get-TargetResource @PSBoundParameters

    $updateCompliant = ($UpdateNow -eq $false -or $Get.UpdateNow -eq $UpdateNow)
    Write-Verbose "updateNow compliant: $updateCompliant"
    $notificationCompliant = (!$Notifications -or $Notifications -eq $Get.Notifications)
    Write-Verbose "notifications compliant: $notificationCompliant"
    $SourceCompliant = (!$Source -or $Source -eq $Get.Source)
    Write-Verbose "service compliant: $SourceCompliant"
    If ($updateCompliant -and $notificationCompliant -and $SourceCompliant) {
        return $true
    } Else {
        return $false
    }
}

function Test-TargetResourceProperties {
    param
    (
        [parameter(Mandatory = $true)]
        [ValidateSet("Yes")]
        [System.String]
        $IsSingleInstance,

        [ValidateSet("Security", "Important", "Optional")]
        [AllowEmptyCollection()]
        [AllowNull()]
        [System.String[]]
        $Category,

        [ValidateSet("Disabled", "ScheduledInstallation")]
        [System.String]
        $Notifications,

        [parameter(Mandatory = $true)]
        [ValidateSet("WindowsUpdate", "MicrosoftUpdate", "WSUS")]
        [System.String]
        $Source,

        [bool]
        $UpdateNow
    )
    $searchStringParams = @{ }
    foreach ($CategoryItem in $Category) {
        $searchStringParams[$CategoryItem.ToLowerInvariant()] = $true
    }

    if ($UpdateNow -and (!$Category -or $Category.Count -eq 0)) {
        Write-Warning 'Defaulting to updating to security updates only. Please specify Category to avoid this warning.'
    } elseif ($searchStringParams.ContainsKey('important') -and !$searchStringParams.ContainsKey('security') ) {
        Write-Warning "Important updates will include security updates. Please include Security in category to avoid this warning."
    } elseif ($searchStringParams.ContainsKey('optional') -and !$searchStringParams.ContainsKey('security') ) {
        Write-Verbose "Optional updates may include security updates."
    }

    if ($Source -eq 'WSUS') {
        throw 'The WSUS service option is not implemented.'
    }
}

Export-ModuleMember -Function *-TargetResource