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