DSCResources/MSFT_xWindowsUpdateAgent/MSFT_xWindowsUpdateAgent.psm1
$script:resourceHelperModulePath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\Modules\DscResource.Common' Import-Module -Name $script:resourceHelperModulePath $script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' $script:WuaSearchString = 'IsAssigned=1 and IsHidden=0 and IsInstalled=0' $script:retryAttempts = 3 $script:retryDelay = 0 $script:lastHResult = 0 $script:errorCount = 0 function Get-WuaServiceManager { return (New-Object -ComObject Microsoft.Update.ServiceManager) } function Add-WuaService { param ( [Parameter(Mandatory = $true)] [System.String] $ServiceId, [Parameter()] [System.Int32] $Flags = 7, [Parameter()] [System.String] $AuthorizationCabPath = [System.String]::Empty ) $wuaServiceManager = Get-WuaServiceManager $wuaServiceManager.AddService2($ServiceId, $Flags, $AuthorizationCabPath) } function Remove-WuaService { param ( [Parameter(Mandatory = $true)] [System.String] $ServiceId ) $wuaServiceManager = Get-WuaServiceManager $wuaServiceManager.RemoveService($ServiceId) } function Get-WuaSearchString { param ( [Parameter()] [System.Management.Automation.SwitchParameter] $security, [Parameter()] [System.Management.Automation.SwitchParameter] $optional, [Parameter()] [System.Management.Automation.SwitchParameter] $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 Assert-Retry { param ( [Parameter()] $ErrorObject ) if ($ErrorObject.Retryable) { if ($ErrorObject.Exception.HResult -ne $script:lastHResult) { $script:lastHResult = $ErrorObject.Exception.HResult $script:errorCount = 0 } if ($script:errorCount++ -lt $script:retryAttempts) { Write-Warning "$($ErrorObject.WarningText) Retrying..." return $true } else { throw $ErrorObject.Exception } Start-Sleep -Seconds $script:retryDelay } else { return $false } } function Get-WuaWrapper { param ( [Parameter()] [ScriptBlock] $tryBlock, [Parameter()] [object[]] $argumentList, [Parameter(ParameterSetName = 'OneValue')] [object] $ExceptionReturnValue = $null ) $script:lastHResult = 0 $script:errorCount = 0 while ($true) { try { return Invoke-Command -ScriptBlock $tryBlock -NoNewScope -ArgumentList $argumentList } catch [System.Runtime.InteropServices.COMException] { $errorObj = [PSCustomObject]@{ Exception = $_.Exception WarningText = '' Retryable = $false } 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 { $errorObj.WarningText = '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 { $errorObj.WarningText = 'Got an error that WU could not resolve the name of the update service.' $errorObj.Retryable = $true } # 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 { $errorObj.WarningText = 'Got an error a request timed out (http status 408 or equivalent) when WU was communicating with the update service.' $errorObj.Retryable = $true } # 0x8024402f -2145107921 WU_E_PT_ECP_SUCCEEDED_WITH_ERRORS External cab file processing completed with some errors. wuerror.h -2145107921 { # No retry needed $errorObj.WarningText = 'Got an error that CAB processing completed with some errors.' return $ExceptionReturnValue } # 0x80244022 -2145107934 WU_E_PT_HTTP_STATUS_SERVICE_UNAVAIL Same as HTTP status 503 - the service is temporarily overloaded. wuerror.h -2145107934 { $errorObj.WarningText = 'Error communicating with the update service, HTTP 503, The service is temporarily overloaded.' $errorObj.Retryable = $true } # 0x80244010 -2145107952 The maximum allowed number of round trips to the server was exceeded -2145107952 { $errorObj.WarningText = 'The maximum allowed number of round trips to the server was exceeded.' $errorObj.Retryable = $true } default { throw } } if (-not (Assert-Retry $errorObj)) { return $ExceptionReturnValue } } } } 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 [System.String]::Empty } function Invoke-WuaDownloadUpdates { param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [object] $UpdateCollection ) $downloader = (Get-WuaSession).CreateUpdateDownloader() $downloader.Updates = $UpdateCollection Write-Verbose -Message 'Downloading updates...' $null = $downloader.Download() } function Invoke-WuaInstallUpdates { param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [object] $UpdateCollection ) $installer = (Get-WuaSession).CreateUpdateInstaller() $installer.Updates = $UpdateCollection Write-Verbose -Message 'Installing updates...' $null = $installer.Install() } function Set-WuaAuNotificationLevel { param ( [Parameter()] [ValidateSet('Not Configured', 'Disabled', 'Notify before download', 'Notify before installation', 'Scheduled installation', 'ScheduledInstallation')] [System.String] $notificationLevel ) $intNotificationLevel = Get-WuaAuNotificationLevelInt -notificationLevel $notificationLevel $settings = Get-WuaAuSettings $settings.NotificationLevel = $intNotificationLevel $settings.Save() } function Get-WuaAuNotificationLevelInt { param ( [Parameter()] [ValidateSet('Not Configured', 'Disabled', 'Notify before download', 'Notify before installation', 'Scheduled installation', 'ScheduledInstallation')] [System.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...' $rebootRequired = (Get-WuaSystemInfo).rebootRequired Write-Verbose -Message "Got rebootRequired: $rebootRequired" 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" return ((Get-WuaSession).CreateUpdateSearcher()).Search($memberSearchString) } -argumentList @($memberSearchString) } function Test-SearchResult { [CmdletBinding()] [OutputType([System.Boolean])] 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, [Parameter()] [ValidateSet('Security', 'Important', 'Optional')] [AllowEmptyCollection()] [AllowNull()] [System.String[]] $Category = @('Security'), [Parameter()] [ValidateSet('Disabled', 'ScheduledInstallation')] [System.String] $Notifications, [Parameter(Mandatory = $true)] [ValidateSet('WindowsUpdate', 'MicrosoftUpdate', 'WSUS')] [System.String] $Source, [Parameter()] [System.Int32] $RetryAttempts = -1, [Parameter()] [System.Int32] $RetryDelay = -1, [Parameter()] [System.Boolean] $UpdateNow = $false ) if ($RetryAttempts -ge 0) { $script:retryAttempts = $RetryAttempts } if ($RetryDelay -ge 0) { $script:retryDelay = $RetryAttempts } 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 { [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')] [CmdletBinding(SupportsShouldProcess = $true)] param ( [Parameter(Mandatory = $true)] [ValidateSet('Yes')] [System.String] $IsSingleInstance, [Parameter()] [ValidateSet('Security', 'Important', 'Optional')] [System.String[]] $Category = @('Security'), [Parameter()] [ValidateSet('Disabled', 'ScheduledInstallation')] [System.String] $Notifications, [Parameter(Mandatory = $true)] [ValidateSet('WindowsUpdate', 'MicrosoftUpdate', 'WSUS')] [System.String] $Source, [Parameter()] [System.Int32] $RetryAttempts = -1, [Parameter()] [System.Int32] $RetryDelay = -1, [Parameter()] [System.Boolean] $UpdateNow = $false ) if ($RetryAttempts -ge 0) { $script:retryAttempts = $RetryAttempts } if ($RetryDelay -ge 0) { $script:retryDelay = $RetryAttempts } 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) { $SearchResult = (Get-WuaSearcher -Category $Category) if ($SearchResult -and $SearchResult.Updates.Count -gt 0) { Write-Verbose -Message 'Installing updates...' #Write Results foreach ($update in $SearchResult.Updates) { $title = $update.Title Write-Verbose -Message "Found update: $Title" } Invoke-WuaDownloadUpdates -UpdateCollection $SearchResult.Updates Invoke-WuaInstallUpdates -UpdateCollection $SearchResult.Updates } else { Write-Verbose -Message 'No updates' } Write-Verbose -Message 'Checking for a reboot...' $rebootRequired = (Get-WuaRebootRequired) if ($rebootRequired) { Write-Verbose -Message 'A reboot was required' $global:DSCMachineStatus = 1 } else { Write-Verbose -Message 'A reboot was NOT required' } } if (!$notificationCompliant) { 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') { Write-Verbose 'Enable the Microsoft Update setting' Add-WuaService -ServiceId '7971f918-a847-4430-9279-4a52d1efe18d' Restart-Service wuauserv -ErrorAction SilentlyContinue } else { 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, [Parameter()] [ValidateSet('Security', 'Important', 'Optional')] [System.String[]] $Category = @('Security'), [Parameter()] [ValidateSet('Disabled', 'ScheduledInstallation')] [System.String] $Notifications, [Parameter(Mandatory = $true)] [ValidateSet('WindowsUpdate', 'MicrosoftUpdate', 'WSUS')] [System.String] $Source, [Parameter()] [System.Int32] $RetryAttempts = -1, [Parameter()] [System.Int32] $RetryDelay = -1, [Parameter()] [System.Boolean] $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, [Parameter()] [ValidateSet('Security', 'Important', 'Optional')] [AllowEmptyCollection()] [AllowNull()] [System.String[]] $Category, [Parameter()] [ValidateSet('Disabled', 'ScheduledInstallation')] [System.String] $Notifications, [Parameter(Mandatory = $true)] [ValidateSet('WindowsUpdate', 'MicrosoftUpdate', 'WSUS')] [System.String] $Source, [Parameter()] [System.Boolean] $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 |