From c5b115edabeb3b4dfe11949d5f1577febbf033ee Mon Sep 17 00:00:00 2001 From: Lord Hepipud Date: Mon, 21 Sep 2026 13:50:23 +0200 Subject: [PATCH 1/4] Adds new handling to run a ScheduledTask in backkground for fetching Windows Updates --- doc/100-General/10-Changelog.md | 4 + doc/160-Features/10-Windows-Update-Offload.md | 179 +++++++++++++++ doc/knowledgebase/IWKB000006.md | 10 +- icinga-powershell-framework.psm1 | 5 + jobs/FetchWindowsUpdates.ps1 | 41 ++++ .../New-IcingaEnvironmentVariable.psm1 | 1 + lib/core/logging/Icinga_EventLog_Enums.psm1 | 6 + .../Disable-IcingaWindowsUpdateOffload.psm1 | 38 ++++ .../Enable-IcingaWindowsUpdateOffload.psm1 | 46 ++++ .../Get-IcingaWindowsUpdateOffload.psm1 | 26 +++ .../Get-IcingaWindowsUpdatePendingList.psm1 | 207 ++++++++++++++++++ ...ngaWindowsScheduledTaskWindowsUpdates.psm1 | 60 +++++ ...ngaWindowsScheduledTaskWindowsUpdates.psm1 | 30 +++ 13 files changed, 651 insertions(+), 2 deletions(-) create mode 100644 doc/160-Features/10-Windows-Update-Offload.md create mode 100644 jobs/FetchWindowsUpdates.ps1 create mode 100644 lib/provider/updates/Disable-IcingaWindowsUpdateOffload.psm1 create mode 100644 lib/provider/updates/Enable-IcingaWindowsUpdateOffload.psm1 create mode 100644 lib/provider/updates/Get-IcingaWindowsUpdateOffload.psm1 create mode 100644 lib/provider/updates/Get-IcingaWindowsUpdatePendingList.psm1 create mode 100644 lib/provider/updates/Register-IcingaWindowsScheduledTaskWindowsUpdates.psm1 create mode 100644 lib/provider/updates/Unregister-IcingaWindowsScheduledTaskWindowsUpdates.psm1 diff --git a/doc/100-General/10-Changelog.md b/doc/100-General/10-Changelog.md index df8aa5cf..9b40fbfc 100644 --- a/doc/100-General/10-Changelog.md +++ b/doc/100-General/10-Changelog.md @@ -11,6 +11,10 @@ Released closed milestones can be found on [GitHub](https://github.com/Icinga/ic [Issues and PRs](https://github.com/Icinga/icinga-powershell-framework/milestone/46) +### Enhancements + +* [#884](https://github.com/Icinga/icinga-powershell-framework/pull/884) Adds new feature, allowing to offload Windows Updates to a background task running as SYSTEM, ensuring Icinga for Windows itself can only run with minimal privileges, while Windows updates can still be fetched by using `Invoke-IcingaCheckUpdates` + ## 1.15.0 (2026-06-30) [Issues and PRs](https://github.com/Icinga/icinga-powershell-framework/milestone/45) diff --git a/doc/160-Features/10-Windows-Update-Offload.md b/doc/160-Features/10-Windows-Update-Offload.md new file mode 100644 index 00000000..83fd37d9 --- /dev/null +++ b/doc/160-Features/10-Windows-Update-Offload.md @@ -0,0 +1,179 @@ +# Windows Update Offload + +The Windows Update Offload feature allows Icinga for Windows to retrieve pending Windows updates using a dedicated background scheduled task running under the `NT AUTHORITY\SYSTEM` account, securely caching the results for the monitoring check plugin. + +**Note:** Before using any of the commands below, you must initialize the Icinga PowerShell Framework inside an administrative PowerShell instance with `icinga -Shell`. + +--- + +## Overview and Motivation + +By default, security best practices dictate that the Icinga Agent should be run with the least necessary privileges—such as `NT AUTHORITY\NetworkService` or a dedicated service account—rather than `NT AUTHORITY\SYSTEM`. + +However, the Windows Update API (`Microsoft.Update.Session` COM object) cannot be queried over remote connections or by unprivileged service accounts without administrative or SYSTEM permissions. Calling update checks in such environments leads to permission errors. + +### Related Knowledge Base Articles + +* **[IWKB000006](../knowledgebase/IWKB000006.md):** The user you are running this command as does not have permission to access the Windows Update ComObject "Microsoft.Update.Session". + +### When to Use Windows Update Offload + +* **Preferred Alternative to Running Everything as SYSTEM:** Rather than elevating the entire Icinga Agent service to `SYSTEM` (which introduces broader security risks), only the update-fetching routine is offloaded to a background task running as `SYSTEM`. +* **Alternative when JEA is not possible:** While [Just Enough Administration (JEA)](../130-JEA/01-Introduction.md) is supported by Icinga for Windows to grant elevated privileges, JEA cannot be implemented or deployed in every environment. If JEA is not viable and you want to monitor Windows Updates without running the Icinga Agent as `SYSTEM`, **Windows Update Offload is the preferred and recommended solution**. + +--- + +## How It Works + +1. **Background Scheduled Task:** + When enabled, a Windows Scheduled Task named `Fetch Windows Updates` is created under `\Icinga\Icinga for Windows\`. This task is configured to start automatically at system startup and runs as `NT AUTHORITY\SYSTEM` (`S-1-5-18`). + +2. **Periodic Update Fetching:** + The task runs `jobs\FetchWindowsUpdates.ps1` in an endless loop. Every **10 minutes (600 seconds)**, it queries the `Microsoft.Update.Session` COM object for pending updates that are not yet installed (`IsInstalled=0`). + +3. **Atomic and Secure Cache Storage:** + The serialized update objects are first written to a temporary file (`pending.xml.tmp`) and then atomically moved to `pending.xml` inside the cache directory (`cache\provider\windows_updates\pending.xml`). Directory and file permissions are strictly secured with `Set-IcingaUserPermissions` so that only `SYSTEM` and the Icinga for Windows service account have access. + +4. **Transparent Check Integration:** + When `Invoke-IcingaCheckUpdates` is executed: + * **Offload Enabled:** The plugin reads the update list directly from the cached XML file, eliminating the need for elevated permissions at check execution time. + * **Offload Disabled:** The plugin queries the Windows Update COM object directly and live. + +--- + +## Cache Age Monitoring & Thresholds + +To ensure you are never monitoring stale or outdated information if the background task fails or hangs, the plugin checks the last write timestamp of the cache file: + +| Cache Age | Check State | Meaning | +| --- | --- | --- | +| `< 20 minutes` | **OK** | Background task is updating the cache normally. | +| `> 20 minutes (1200s)` | **WARNING** | Data is stale. The background task may have been delayed or failed its last run. | +| `> 30 minutes (1800s)` | **CRITICAL** | Data is severely outdated. The background task is likely hung, terminated, disabled, or encountering persistent errors. | + +The check output displays: +* **`Last Update Check`:** Time offset in seconds since the cache file was last written. +* **`Last Fetch Timestamp`:** The exact UTC timestamp when the cache was written (e.g. `2026-09-21 11:30:00 UTC`). + +--- + +## Enabling Windows Update Offload + +To enable the feature, open an administrative PowerShell prompt and run: + +```powershell +Enable-IcingaWindowsUpdateOffload; +``` + +```text +[Notice]: The task "Fetch Windows Updates" has been successfully registered at location "\Icinga\Icinga for Windows\". +``` + +This will: +* Set `Framework.WindowsUpdateOffload` to `$TRUE` in your framework configuration. +* Register the scheduled task `Fetch Windows Updates` under `\Icinga\Icinga for Windows\`. +* Automatically trigger the initial run of the task so that the cache file is created immediately. + +### Silent Mode + +If you are automating the setup, you can suppress console output by passing the `-Silent` switch: + +```powershell +Enable-IcingaWindowsUpdateOffload -Silent; +``` + +--- + +## Checking Offload Status + +You can verify whether the feature is currently active with `Get-IcingaWindowsUpdateOffload`: + +```powershell +Get-IcingaWindowsUpdateOffload; +``` + +```text +True +``` + +You can also check the state of the scheduled task using the standard PowerShell cmdlet: + +```powershell +Get-ScheduledTask -TaskName 'Fetch Windows Updates' -TaskPath '\Icinga\Icinga for Windows\'; +``` + +```text +TaskPath TaskName State +-------- -------- ----- +\Icinga\Icinga for Windows\ Fetch Windows Updates Running +``` + +--- + +## Disabling Windows Update Offload + +To disable the offload feature, open an administrative PowerShell prompt and run: + +```powershell +Disable-IcingaWindowsUpdateOffload; +``` + +```text +[Notice]: The "Fetch Windows Updates" task was removed from the system. +``` + +This will: +* Set `Framework.WindowsUpdateOffload` to `$FALSE`. +* Stop and unregister the `Fetch Windows Updates` scheduled task. +* Remove the `pending.xml` cache file to prevent stale data from being kept on disk. +* Revert `Invoke-IcingaCheckUpdates` to querying updates directly. + +--- + +## Check Plugin Example Output + +When `Invoke-IcingaCheckUpdates` runs with Windows Update Offload enabled: + +```powershell +Invoke-IcingaCheckUpdates; +``` + +```text +[OK] Windows Updates: 8 Ok (All must be [OK]) +\_ [INFO] Last Fetch Timestamp: 2026-09-21 11:54:18 UTC +\_ [OK] Last Update Check: 1m +\_ [INFO] Microsoft Defender (All must be [OK]) + \_ [INFO] Security Intelligence Update for Microsoft Defender Antivirus - KB2267602 (Version 1.459.318.0) - Current Channel (Broad) [9/21/2026 12:00:00 AM]: Nothing + \_ [INFO] Update Count: 1c +\_ [INFO] Other (All must be [OK]) + \_ [INFO] Update Count: 0c +\_ [INFO] Reboot Pending: No +\_ [INFO] Security Updates (All must be [OK]) + \_ [INFO] 2026-09 Security Update (KB5129195) (26200.9457) [9/14/2026 12:00:00 AM]: Nothing + \_ [INFO] Update Count: 1c +\_ [INFO] Total Pending Updates: 2c +\_ [INFO] Update Rollups (All must be [OK]) + \_ [INFO] Update Count: 0c +``` + +If the background task stops running and the cache exceeds the threshold: + +```text +[WARNING] Windows Updates: 1 Warning 7 Ok [WARNING] Last Update Check (All must be [OK]) +\_ [INFO] Last Fetch Timestamp: 2026-09-21 11:59:18 UTC +\_ [WARNING] Last Update Check: Value 26.37m is greater than threshold 20m +\_ [INFO] Microsoft Defender (All must be [OK]) + \_ [INFO] Security Intelligence Update for Microsoft Defender Antivirus - KB2267602 (Version 1.459.318.0) - Current Channel (Broad) [9/21/2026 12:00:00 AM]: Nothing + \_ [INFO] Update Count: 1c +\_ [INFO] Other (All must be [OK]) + \_ [INFO] Update Count: 0c +\_ [INFO] Reboot Pending: Yes +\_ [INFO] Security Updates (All must be [OK]) + \_ [INFO] 2026-09 Security Update (KB5129195) (26200.9457) [9/14/2026 12:00:00 AM]: Nothing + \_ [INFO] Update Count: 1c +\_ [INFO] Total Pending Updates: 2c +\_ [INFO] Update Rollups (All must be [OK]) + \_ [INFO] Update Count: 0c +``` + +This immediately signals that the background process requires attention. diff --git a/doc/knowledgebase/IWKB000006.md b/doc/knowledgebase/IWKB000006.md index 558380b5..628e7fcb 100644 --- a/doc/knowledgebase/IWKB000006.md +++ b/doc/knowledgebase/IWKB000006.md @@ -17,6 +17,12 @@ The Windows COM Object is rejecting every access to these information over remot ## Solution -Right now there is no solution available for this problem. Microsoft is not allowing to grant permission to these objects over remote connections, which makes it impossible to use them. In addition there is no proper alternative for fetching pending Windows Updates and Hotfixes. +To resolve this issue without running the entire Icinga Agent service as `NT AUTHORITY\SYSTEM`, you can use the [Windows Update Offload](https://icinga.com/docs/icinga-for-windows/latest/doc/160-Features/10-Windows-Update-Offload) feature. -A possible fix which is suggested online is to add a scheduled task, running the command after being triggered by our execution and afterwards fetching the result from the task. This solution requires more research, testing and development. +By enabling Windows Update Offload, a dedicated background scheduled task running as `SYSTEM` fetches pending Windows updates periodically and caches the results securely. The check plugin then reads from this cache without requiring elevated permissions or direct COM object access during check execution: + +```powershell +Enable-IcingaWindowsUpdateOffload; +``` + +An alternative solution is to use [JEA](https://icinga.com/docs/icinga-for-windows/latest/doc/130-JEA/01-JEA-Profiles/) \ No newline at end of file diff --git a/icinga-powershell-framework.psm1 b/icinga-powershell-framework.psm1 index 90b1ddb6..7c9d3e38 100644 --- a/icinga-powershell-framework.psm1 +++ b/icinga-powershell-framework.psm1 @@ -44,6 +44,11 @@ function Use-Icinga() Enable-IcingaFrameworkDebugMode; } + # Enable Windows Update Offload in case it is enabled in our config + if (Get-IcingaWindowsUpdateOffload) { + Enable-IcingaWindowsUpdateOffload -Silent; + } + $EventLogMessages = Invoke-IcingaNamespaceCmdlets -Command 'Register-IcingaEventLogMessages*'; foreach ($entry in $EventLogMessages.Values) { foreach ($event in $entry.Keys) { diff --git a/jobs/FetchWindowsUpdates.ps1 b/jobs/FetchWindowsUpdates.ps1 new file mode 100644 index 00000000..e218ac85 --- /dev/null +++ b/jobs/FetchWindowsUpdates.ps1 @@ -0,0 +1,41 @@ +Use-Icinga; + +$UpdateFile = Join-Path -Path (Get-IcingaCacheDir) -ChildPath 'provider\windows_updates\pending.xml'; +$UpdateTmpFile = Join-Path -Path (Get-IcingaCacheDir) -ChildPath 'provider\windows_updates\pending.xml.tmp'; + +# In case the file does not yet exist, create it once and ensure we update the permissions that +# noone besides the SYSTEM and Icinga for Windows user can access them +if (-not (Test-Path -Path $UpdateFile)) { + Set-IcingaUserPermissions; +} + +while ($TRUE) { + try { + #$WindowsUpdates = Get-IcingaWindowsUpdatePendingList -AsTask; + # Fetch all informations about installed updates and add them + $WindowsUpdates = New-Object -ComObject 'Microsoft.Update.Session' -ErrorAction Stop; + $SearchIndex = $WindowsUpdates.CreateUpdateSearcher(); + # Get a list of current pending updates which are not yet installed on the system + $Pending = $SearchIndex.Search('IsInstalled=0'); + $XMLObj = [System.Management.Automation.PSSerializer]::Serialize($Pending.Updates, 3); + + # First write the new update data to a tmp file to avoid race conditions + Write-IcingaFileSecure -File $UpdateTmpFile -Value $XMLObj; + + # Now simply move the new tmp file to the target file - keep doing this until the file does not exist anymore + # This atomic operation ensures that we do not have a corrupt file on disk + while ((Test-Path -Path $UpdateTmpFile)) { + Move-Item -Path $UpdateTmpFile -Destination $UpdateFile -Force -ErrorAction SilentlyContinue; + Start-Sleep -Seconds 1; + } + } catch { + Write-IcingaEventMessage -EventId 1200 -Namespace 'Framework' -Objects $UpdateFile, $XMLObj, $_.Exception.Message; + } finally { + $WindowsUpdates = $null; + $SearchIndex = $null; + $Pending = $null; + $XMLObj = $null; + # Fetch Windows Updates every 10 minutes (600 seconds) + Start-Sleep -Seconds 600; + } +} diff --git a/lib/core/framework/New-IcingaEnvironmentVariable.psm1 b/lib/core/framework/New-IcingaEnvironmentVariable.psm1 index 88852410..a7c5a1e8 100644 --- a/lib/core/framework/New-IcingaEnvironmentVariable.psm1 +++ b/lib/core/framework/New-IcingaEnvironmentVariable.psm1 @@ -77,6 +77,7 @@ function New-IcingaEnvironmentVariable() $Global:Icinga.Protected.Add('DeveloperMode', $FALSE); $Global:Icinga.Protected.Add('DebugMode', $FALSE); + $Global:Icinga.Protected.Add('WindowsUpdateOffload', $FALSE); $Global:Icinga.Protected.Add('JEAContext', $FALSE); $Global:Icinga.Protected.Add('RunAsDaemon', $FALSE); $Global:Icinga.Protected.Add('Minimal', $FALSE); diff --git a/lib/core/logging/Icinga_EventLog_Enums.psm1 b/lib/core/logging/Icinga_EventLog_Enums.psm1 index 16e2921c..9bd6ae95 100644 --- a/lib/core/logging/Icinga_EventLog_Enums.psm1 +++ b/lib/core/logging/Icinga_EventLog_Enums.psm1 @@ -44,6 +44,12 @@ if ($null -eq $IcingaEventLogEnums -Or $IcingaEventLogEnums.ContainsKey('Framewo 'Details' = 'Icinga for Windows could not read the specified cache file, as the content seems to be corrupt. This happens mostly in case of unexpected shutdowns or terminations during the write process.'; 'EventId' = 1104; }; + 1200 = @{ + 'EntryType' = 'Error'; + 'Message' = 'Unable to fetch Windows Updates from background task'; + 'Details' = 'Icinga for Windows failed to fetch the pending Windows updates by using the scheduled background task due to an error.'; + 'EventId' = 1200; + }; 1400 = @{ 'EntryType' = 'Error'; 'Message' = 'Icinga for Windows background daemon not found'; diff --git a/lib/provider/updates/Disable-IcingaWindowsUpdateOffload.psm1 b/lib/provider/updates/Disable-IcingaWindowsUpdateOffload.psm1 new file mode 100644 index 00000000..aebfc9ca --- /dev/null +++ b/lib/provider/updates/Disable-IcingaWindowsUpdateOffload.psm1 @@ -0,0 +1,38 @@ +<# +.SYNOPSIS + Disables the Windows Update Offload feature. +.DESCRIPTION + Disables the Windows Update Offload feature. The background scheduled task + ('Fetch Windows Updates') is stopped and unregistered, the internal configuration + is set to FALSE, and the cached XML file is safely removed. + + Subsequent check executions will query the Windows Update COM object directly, + which requires appropriate permissions. + + This command requires administrative privileges. +.FUNCTIONALITY + Disables Windows Update Offload and cleans up tasks and cache files. +.EXAMPLE + PS>Disable-IcingaWindowsUpdateOffload; +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Disable-IcingaWindowsUpdateOffload() +{ + # Only run this if we use an administrative shell + if (-not (Test-AdministrativeShell)) { + Write-IcingaConsoleError 'You require administrative privileges to run this command'; + return; + } + + # Disable scheduled tasks and clear internal config values + $Global:Icinga.Protected.WindowsUpdateOffload = $FALSE; + Set-IcingaPowerShellConfig -Path 'Framework.WindowsUpdateOffload' -Value $FALSE; + + Unregister-IcingaWindowsScheduledTaskWindowsUpdates; + + # Remove the XML file containing the update information to not store old data + $UpdateFile = Join-Path -Path (Get-IcingaCacheDir) -ChildPath 'provider\windows_updates\pending.xml'; + Remove-ItemSecure -Path $UpdateFile -Retries 5 -Force | Out-Null; +} diff --git a/lib/provider/updates/Enable-IcingaWindowsUpdateOffload.psm1 b/lib/provider/updates/Enable-IcingaWindowsUpdateOffload.psm1 new file mode 100644 index 00000000..88925538 --- /dev/null +++ b/lib/provider/updates/Enable-IcingaWindowsUpdateOffload.psm1 @@ -0,0 +1,46 @@ +<# +.SYNOPSIS + Enables the Windows Update Offload feature. +.DESCRIPTION + Enables the Windows Update Offload feature. When enabled, a Windows Scheduled Task + ('Fetch Windows Updates') running as SYSTEM will fetch pending Windows updates in + the background every 10 minutes and save them securely to the cache directory. + + Check plugins will read the update state from the cache file instead of querying + the Windows Update COM-Object live. This allows running the Icinga Agent service + as a non-SYSTEM user without requiring JEA. + + This command requires administrative privileges. +.FUNCTIONALITY + Enables Windows Update Offload and registers the background task. +.PARAMETER Silent + Suppresses console error and notice messages. +.EXAMPLE + PS>Enable-IcingaWindowsUpdateOffload; +.EXAMPLE + PS>Enable-IcingaWindowsUpdateOffload -Silent; +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Enable-IcingaWindowsUpdateOffload() +{ + param ( + [switch]$Silent = $false + ); + + # Only run this if we use an administrative shell + if (-not (Test-AdministrativeShell)) { + if (-not $Silent) { + Write-IcingaConsoleError 'You require administrative privileges to run this command'; + } + + return; + } + + # Register the scheduled task and set internal config values + $Global:Icinga.Protected.WindowsUpdateOffload = $TRUE; + Set-IcingaPowerShellConfig -Path 'Framework.WindowsUpdateOffload' -Value $TRUE; + + Register-IcingaWindowsScheduledTaskWindowsUpdates -Silent:$Silent; +} diff --git a/lib/provider/updates/Get-IcingaWindowsUpdateOffload.psm1 b/lib/provider/updates/Get-IcingaWindowsUpdateOffload.psm1 new file mode 100644 index 00000000..f1e2acd9 --- /dev/null +++ b/lib/provider/updates/Get-IcingaWindowsUpdateOffload.psm1 @@ -0,0 +1,26 @@ +<# +.SYNOPSIS + Returns the current configuration status of the Windows Update Offload feature. +.DESCRIPTION + Checks if the Windows Update Offload feature is currently enabled in the + Icinga PowerShell configuration under 'Framework.WindowsUpdateOffload'. +.FUNCTIONALITY + Retrieves the Windows Update Offload feature state. +.OUTPUTS + System.Boolean +.EXAMPLE + PS>Get-IcingaWindowsUpdateOffload; +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Get-IcingaWindowsUpdateOffload() +{ + $UpdateOffload = Get-IcingaPowerShellConfig -Path 'Framework.WindowsUpdateOffload'; + + if ($null -eq $UpdateOffload) { + return $FALSE; + } + + return $UpdateOffload; +} diff --git a/lib/provider/updates/Get-IcingaWindowsUpdatePendingList.psm1 b/lib/provider/updates/Get-IcingaWindowsUpdatePendingList.psm1 new file mode 100644 index 00000000..3620c12e --- /dev/null +++ b/lib/provider/updates/Get-IcingaWindowsUpdatePendingList.psm1 @@ -0,0 +1,207 @@ +<# +.SYNOPSIS + Retrieves a list of pending Windows updates, either live or from the offloaded cache. +.DESCRIPTION + Queries pending Windows updates that are not yet installed on the system. + + When Windows Update Offload is enabled ('Framework.WindowsUpdateOffload') and the '-AsTask' + switch is NOT specified, this function reads and deserializes the update information from + the XML cache file generated by the background scheduled task. It also evaluates the cache file + write timestamp ('fetched' as offset in seconds, 'fetched_hr' as formatted UTC string). + + If offload is disabled or '-AsTask' is used, it queries the 'Microsoft.Update.Session' + COM object directly. + + The updates are categorized into security updates, defender definition updates, + update rollups, and other updates. +.FUNCTIONALITY + Fetches and categorizes pending Windows updates for monitoring checks. +.PARAMETER AsTask + Forces a direct live query of the Windows Update COM object, bypassing the cache file. +.PARAMETER UpdateFilter + Optional array of wildcard strings to filter updates by title when calculating the count. +.OUTPUTS + [hashtable] A hashtable containing update counts, reboot requirements, categorized update details, + error messages, and fetch timestamps. +.EXAMPLE + PS>Get-IcingaWindowsUpdatePendingList; +.EXAMPLE + PS>Get-IcingaWindowsUpdatePendingList -UpdateFilter @('*Security*'); +.EXAMPLE + PS>Get-IcingaWindowsUpdatePendingList -AsTask; +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Get-IcingaWindowsUpdatePendingList() +{ + param ( + [switch]$AsTask = $false, + [array]$UpdateFilter = @() + ); + + [hashtable]$PendingUpdates = @{ }; + [hashtable]$PendingUpdateNameCache = @{ }; + $Pending = $null; + + # If we are not running as task, it means a check plugin is fetching the information. + # We should test if the XML file is present deserialize the object. As the 1:1 serialize the values from + # this function, we can entirely use the output to proceed with the update plugin output + if (-not $AsTask -and $Global:Icinga.Protected.WindowsUpdateOffload) { + $UpdateFile = Join-Path -Path (Get-IcingaCacheDir) -ChildPath 'provider\windows_updates\pending.xml'; + + if (-not (Test-Path -Path $UpdateFile)) { + $PendingUpdates.Add('count', 0); + $PendingUpdates.Add('error', 'The Icinga for Windows pending update xml file is not yet present.'); + + return $PendingUpdates; + } + + $WindowsUpdates = Read-IcingaFileSecure -File $UpdateFile; + $Pending = ([System.Management.Automation.PSSerializer]::Deserialize($WindowsUpdates)); + $LastWriteTime = [System.IO.File]::GetLastWriteTimeUtc($UpdateFile); + $PendingUpdates.Add('fetched_hr', $LastWriteTime.ToString("yyyy-MM-dd HH:mm:ss 'UTC'")); + $PendingUpdates.Add('fetched', (Get-IcingaUnixTimeOffsetNow -UnixTime ([DateTimeOffset]$LastWriteTime).ToUnixTimeSeconds())); + } else { + # Fetch all informations about installed updates and add them + try { + $WindowsUpdates = New-Object -ComObject "Microsoft.Update.Session" -ErrorAction Stop; + $SearchIndex = $WindowsUpdates.CreateUpdateSearcher(); + # Get a list of current pending updates which are not yet installed on the system + $Pending = $SearchIndex.Search("IsInstalled=0"); + $Pending = $Pending.Updates; + } catch { + Exit-IcingaThrowException -ExceptionType 'Permission' -ExceptionThrown $IcingaExceptions.Permission.WindowsUpdate -Force; + } + + $PendingUpdates.Add('fetched_hr', [DateTime]::UtcNow.ToString("yyyy-MM-dd HH:mm:ss 'UTC'")); + $PendingUpdates.Add('fetched', 0); + } + + try { + $PendingUpdates.Add('count', 0); + $PendingUpdates.Add( + 'RebootPending', + (Test-Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired") + ); + $PendingUpdates.Add( + 'updates', + @{ + 'security' = @{ }; + 'defender' = @{ }; + 'rollups' = @{ }; + 'other' = @{ }; + } + ); + + foreach ($update in $Pending) { + [hashtable]$PendingUpdateDetails = @{ }; + $PendingUpdateDetails.Add('Title', $update.Title); + $PendingUpdateDetails.Add('Category', $null); + $PendingUpdateDetails.Add('Deadline', $update.Deadline); + $PendingUpdateDetails.Add('Description', $update.Description); + $PendingUpdateDetails.Add('IsBeta', $update.IsBeta); + $PendingUpdateDetails.Add('IsDownloaded', $update.IsDownloaded); + $PendingUpdateDetails.Add('IsHidden', $update.IsHidden); + $PendingUpdateDetails.Add('IsInstalled', $update.IsInstalled); + $PendingUpdateDetails.Add('IsMandatory', $update.IsMandatory); + $PendingUpdateDetails.Add('IsUninstallable', $update.IsUninstallable); + $PendingUpdateDetails.Add('Languages', $update.Languages); + $PendingUpdateDetails.Add('LastDeploymentChangeTime', $update.LastDeploymentChangeTime); + $PendingUpdateDetails.Add('MaxDownloadSize', $update.MaxDownloadSize); + $PendingUpdateDetails.Add('MinDownloadSize', $update.MinDownloadSize); + $PendingUpdateDetails.Add('MoreInfoUrls', $update.MoreInfoUrls); + $PendingUpdateDetails.Add('MsrcSeverity', $update.MsrcSeverity); + $PendingUpdateDetails.Add('RecommendedCpuSpeed', $update.RecommendedCpuSpeed); + $PendingUpdateDetails.Add('RecommendedHardDiskSpace', $update.RecommendedHardDiskSpace); + $PendingUpdateDetails.Add('RecommendedMemory', $update.RecommendedMemory); + $PendingUpdateDetails.Add('ReleaseNotes', $update.ReleaseNotes); + $PendingUpdateDetails.Add('SecurityBulletinIDs', $update.SecurityBulletinIDs); + $PendingUpdateDetails.Add('SupersededUpdateIDs', $update.SupersededUpdateIDs); + $PendingUpdateDetails.Add('SupportUrl', $update.SupportUrl); + $PendingUpdateDetails.Add('Type', $update.Type); + $PendingUpdateDetails.Add('UninstallationNotes', $update.UninstallationNotes); + $PendingUpdateDetails.Add('UninstallationBehavior', $update.UninstallationBehavior); + $PendingUpdateDetails.Add('UninstallationSteps', $update.UninstallationSteps); + $PendingUpdateDetails.Add('KBArticleIDs', $update.KBArticleIDs); + $PendingUpdateDetails.Add('DeploymentAction', $update.DeploymentAction); + $PendingUpdateDetails.Add('DownloadPriority', $update.DownloadPriority); + $PendingUpdateDetails.Add('RebootRequired', $update.RebootRequired); + $PendingUpdateDetails.Add('IsPresent', $update.IsPresent); + $PendingUpdateDetails.Add('CveIDs', $update.CveIDs); + $PendingUpdateDetails.Add('BrowseOnly', $update.BrowseOnly); + $PendingUpdateDetails.Add('PerUser', $update.PerUser); + $PendingUpdateDetails.Add('AutoSelection', $update.AutoSelection); + $PendingUpdateDetails.Add('AutoDownload', $update.AutoDownload); + + if ($UpdateFilter.Count -ne 0) { + foreach ($filter in $UpdateFilter) { + if ($update.Title -Like $filter) { + $PendingUpdates.count += 1; + break; + } + } + } else { + $PendingUpdates.count += 1; + } + + [string]$name = [string]::Format('{0} [{1}]', $update.Title, $update.LastDeploymentChangeTime); + + if ($PendingUpdateNameCache.ContainsKey($name) -eq $FALSE) { + $PendingUpdateNameCache.Add($name, 1); + } else { + $PendingUpdateNameCache[$name] += 1; + $name = [string]::Format('{0} ({1})', $name, $PendingUpdateNameCache[$name]); + } + + [bool]$IsSecurity = $FALSE; + [bool]$IsDefender = $FALSE; + [bool]$IsRollUp = $FALSE; + + foreach ($category in $update.Categories) { + if ($category.Name -eq 'Update Rollups') { + $IsRollUp = $TRUE; + $PendingUpdateDetails.Category = $category; + } + if ($category.Name -eq 'Definition Updates' -Or $category.Name -eq 'Microsoft Defender Antivirus') { + $IsDefender = $TRUE; + $PendingUpdateDetails.Category = $category; + break; + } + if ($category.Name -eq 'Security Updates') { + $IsSecurity = $TRUE; + $PendingUpdateDetails.Category = $category; + break; + } + } + + if ($null -eq $PendingUpdateDetails.Category) { + $PendingUpdateDetails.Category = $update.Categories[0]; + } + + if ($IsSecurity) { + $PendingUpdates.updates.security.Add($name, $PendingUpdateDetails); + continue; + } + if ($IsDefender) { + $PendingUpdates.updates.defender.Add($name, $PendingUpdateDetails); + continue; + } + if ($IsRollUp) { + $PendingUpdates.updates.rollups.Add($name, $PendingUpdateDetails); + continue; + } + + $PendingUpdates.updates.other.Add($name, $PendingUpdateDetails); + } + } catch { + if ($PendingUpdates.ContainsKey('Count') -eq $FALSE) { + $PendingUpdates.Add('count', 0); + } else { + $PendingUpdates['count'] = 0; + } + $PendingUpdates.Add('error', $_.Exception.Message); + } + + return $PendingUpdates; +} diff --git a/lib/provider/updates/Register-IcingaWindowsScheduledTaskWindowsUpdates.psm1 b/lib/provider/updates/Register-IcingaWindowsScheduledTaskWindowsUpdates.psm1 new file mode 100644 index 00000000..49f691a3 --- /dev/null +++ b/lib/provider/updates/Register-IcingaWindowsScheduledTaskWindowsUpdates.psm1 @@ -0,0 +1,60 @@ +<# +.SYNOPSIS + Registers and starts the Windows Scheduled Task for fetching Windows Updates. +.DESCRIPTION + Creates a Windows Scheduled Task named 'Fetch Windows Updates' under '\Icinga\Icinga for Windows\'. + The task is configured to run at system startup under the 'NT AUTHORITY\SYSTEM' account ('S-1-5-18') + with highest privileges. + + It executes 'jobs\FetchWindowsUpdates.ps1', which continuously fetches pending Windows updates + every 10 minutes and writes the serialized data atomically into the cache directory. + Immediately after registration, the task is started. +.FUNCTIONALITY + Registers and starts the Windows Update background fetch scheduled task. +.PARAMETER Silent + Suppresses console notice and warning messages. +.PARAMETER Force + Forces the re-creation of the scheduled task if it already exists. +.EXAMPLE + PS>Register-IcingaWindowsScheduledTaskWindowsUpdates; +.EXAMPLE + PS>Register-IcingaWindowsScheduledTaskWindowsUpdates -Force; +.EXAMPLE + PS>Register-IcingaWindowsScheduledTaskWindowsUpdates -Silent; +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Register-IcingaWindowsScheduledTaskWindowsUpdates() +{ + param ( + [switch]$Silent = $false, + [switch]$Force = $FALSE + ); + + [string]$TaskName = 'Fetch Windows Updates'; + [string]$TaskPath = '\Icinga\Icinga for Windows\'; + + $FetchUpdatesTask = Get-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath -ErrorAction SilentlyContinue; + + if ($null -ne $FetchUpdatesTask -And $Force -eq $FALSE) { + if (-not $Silent) { + Write-IcingaConsoleWarning -Message 'The {0} task is already present. User -Force to enforce the re-creation' -Objects $TaskName; + } + return; + } + + $ScriptPath = Join-Path -Path (Get-IcingaFrameworkRootPath) -ChildPath '\jobs\FetchWindowsUpdates.ps1'; + $TaskTrigger = New-ScheduledTaskTrigger -AtStartup; + $TaskAction = New-ScheduledTaskAction -Execute 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe' -Argument ([string]::Format("-NoProfile -WindowStyle Hidden -Command &{{ & '{0}' }}", $ScriptPath)); + $TaskPrincipal = New-ScheduledTaskPrincipal -UserId 'S-1-5-18' -RunLevel 'Highest' -LogonType ServiceAccount; + $TaskSettings = New-ScheduledTaskSettingsSet -DontStopIfGoingOnBatteries -AllowStartIfOnBatteries -StartWhenAvailable; + + Register-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath -Force -Principal $TaskPrincipal -Action $TaskAction -Trigger $TaskTrigger -Settings $TaskSettings | Out-Null; + # Start the task directly after creation + Start-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath; + + if (-not $Silent) { + Write-IcingaConsoleNotice -Message 'The task "{0}" has been successfully registered at location "{1}".' -Objects $TaskName, $TaskPath; + } +} diff --git a/lib/provider/updates/Unregister-IcingaWindowsScheduledTaskWindowsUpdates.psm1 b/lib/provider/updates/Unregister-IcingaWindowsScheduledTaskWindowsUpdates.psm1 new file mode 100644 index 00000000..8838b78d --- /dev/null +++ b/lib/provider/updates/Unregister-IcingaWindowsScheduledTaskWindowsUpdates.psm1 @@ -0,0 +1,30 @@ +<# +.SYNOPSIS + Stops and unregisters the Windows Scheduled Task for fetching Windows Updates. +.DESCRIPTION + Stops the currently running scheduled task 'Fetch Windows Updates' under + '\Icinga\Icinga for Windows\' and unregisters it from the Windows Task Scheduler. +.FUNCTIONALITY + Unregisters the Windows Update background fetch scheduled task. +.EXAMPLE + PS>Unregister-IcingaWindowsScheduledTaskWindowsUpdates; +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Unregister-IcingaWindowsScheduledTaskWindowsUpdates() +{ + [string]$TaskName = 'Fetch Windows Updates'; + [string]$TaskPath = '\Icinga\Icinga for Windows\'; + + $FetchUpdatesTask = Get-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath -ErrorAction SilentlyContinue; + + if ($null -eq $FetchUpdatesTask) { + Write-IcingaConsoleNotice -Message 'The "{0}" task is not present on this system.' -Objects $TaskName; + return; + } + + Stop-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath | Out-Null; + Unregister-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath -Confirm:$FALSE -ErrorAction SilentlyContinue | Out-Null; + Write-IcingaConsoleNotice -Message 'The "{0}" task was removed from the system.' -Objects $TaskName; +} From d448e46a59be7b34817cce5a2f1c5e794e54b48d Mon Sep 17 00:00:00 2001 From: Lord Hepipud Date: Thu, 24 Sep 2026 16:42:48 +0200 Subject: [PATCH 2/4] Fixes missing provider folder creation --- jobs/FetchWindowsUpdates.ps1 | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/jobs/FetchWindowsUpdates.ps1 b/jobs/FetchWindowsUpdates.ps1 index e218ac85..62edba7a 100644 --- a/jobs/FetchWindowsUpdates.ps1 +++ b/jobs/FetchWindowsUpdates.ps1 @@ -1,11 +1,13 @@ Use-Icinga; -$UpdateFile = Join-Path -Path (Get-IcingaCacheDir) -ChildPath 'provider\windows_updates\pending.xml'; -$UpdateTmpFile = Join-Path -Path (Get-IcingaCacheDir) -ChildPath 'provider\windows_updates\pending.xml.tmp'; +$UpdateDir = Join-Path -Path (Get-IcingaCacheDir) -ChildPath 'provider\windows_updates'; +$UpdateFile = Join-Path -Path $UpdateDir -ChildPath 'pending.xml'; +$UpdateTmpFile = Join-Path -Path $UpdateDir -ChildPath 'pending.xml.tmp'; # In case the file does not yet exist, create it once and ensure we update the permissions that # noone besides the SYSTEM and Icinga for Windows user can access them -if (-not (Test-Path -Path $UpdateFile)) { +if (-not (Test-Path -Path $UpdateDir)) { + New-Item -Path $UpdateDir -ItemType Directory | Out-Null; Set-IcingaUserPermissions; } From 0b06db53489fe506364de3539ea7493dff1b3828 Mon Sep 17 00:00:00 2001 From: Lord Hepipud Date: Fri, 25 Sep 2026 14:19:19 +0200 Subject: [PATCH 3/4] Fixes enable/disable variable not being set propery without administrative shell --- lib/provider/updates/Disable-IcingaWindowsUpdateOffload.psm1 | 5 +++-- lib/provider/updates/Enable-IcingaWindowsUpdateOffload.psm1 | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/provider/updates/Disable-IcingaWindowsUpdateOffload.psm1 b/lib/provider/updates/Disable-IcingaWindowsUpdateOffload.psm1 index aebfc9ca..ce5192e0 100644 --- a/lib/provider/updates/Disable-IcingaWindowsUpdateOffload.psm1 +++ b/lib/provider/updates/Disable-IcingaWindowsUpdateOffload.psm1 @@ -20,14 +20,15 @@ function Disable-IcingaWindowsUpdateOffload() { + # Disable scheduled tasks and clear internal config values + $Global:Icinga.Protected.WindowsUpdateOffload = $FALSE; + # Only run this if we use an administrative shell if (-not (Test-AdministrativeShell)) { Write-IcingaConsoleError 'You require administrative privileges to run this command'; return; } - # Disable scheduled tasks and clear internal config values - $Global:Icinga.Protected.WindowsUpdateOffload = $FALSE; Set-IcingaPowerShellConfig -Path 'Framework.WindowsUpdateOffload' -Value $FALSE; Unregister-IcingaWindowsScheduledTaskWindowsUpdates; diff --git a/lib/provider/updates/Enable-IcingaWindowsUpdateOffload.psm1 b/lib/provider/updates/Enable-IcingaWindowsUpdateOffload.psm1 index 88925538..bbade337 100644 --- a/lib/provider/updates/Enable-IcingaWindowsUpdateOffload.psm1 +++ b/lib/provider/updates/Enable-IcingaWindowsUpdateOffload.psm1 @@ -29,6 +29,9 @@ function Enable-IcingaWindowsUpdateOffload() [switch]$Silent = $false ); + # Register the scheduled task and set internal config values + $Global:Icinga.Protected.WindowsUpdateOffload = $TRUE; + # Only run this if we use an administrative shell if (-not (Test-AdministrativeShell)) { if (-not $Silent) { @@ -38,8 +41,6 @@ function Enable-IcingaWindowsUpdateOffload() return; } - # Register the scheduled task and set internal config values - $Global:Icinga.Protected.WindowsUpdateOffload = $TRUE; Set-IcingaPowerShellConfig -Path 'Framework.WindowsUpdateOffload' -Value $TRUE; Register-IcingaWindowsScheduledTaskWindowsUpdates -Silent:$Silent; From 1d7ef1e74da7463332b35d5b4fafc8d96415749b Mon Sep 17 00:00:00 2001 From: Lord Hepipud Date: Fri, 25 Sep 2026 16:40:56 +0200 Subject: [PATCH 4/4] Fixes serializer/deserializer for not relying on COM-Objects, which could sometimes cause information being discarded --- jobs/FetchWindowsUpdates.ps1 | 13 +- .../Get-IcingaWindowsUpdatePendingList.psm1 | 15 +- .../updates/Get-IcingaWindowsUpdateRaw.psm1 | 132 ++++++++++++++++++ 3 files changed, 141 insertions(+), 19 deletions(-) create mode 100644 lib/provider/updates/Get-IcingaWindowsUpdateRaw.psm1 diff --git a/jobs/FetchWindowsUpdates.ps1 b/jobs/FetchWindowsUpdates.ps1 index 62edba7a..4ee05469 100644 --- a/jobs/FetchWindowsUpdates.ps1 +++ b/jobs/FetchWindowsUpdates.ps1 @@ -15,11 +15,8 @@ while ($TRUE) { try { #$WindowsUpdates = Get-IcingaWindowsUpdatePendingList -AsTask; # Fetch all informations about installed updates and add them - $WindowsUpdates = New-Object -ComObject 'Microsoft.Update.Session' -ErrorAction Stop; - $SearchIndex = $WindowsUpdates.CreateUpdateSearcher(); - # Get a list of current pending updates which are not yet installed on the system - $Pending = $SearchIndex.Search('IsInstalled=0'); - $XMLObj = [System.Management.Automation.PSSerializer]::Serialize($Pending.Updates, 3); + $Updates = Get-IcingaWindowsUpdateRaw; + $XMLObj = [System.Management.Automation.PSSerializer]::Serialize($Updates, 3); # First write the new update data to a tmp file to avoid race conditions Write-IcingaFileSecure -File $UpdateTmpFile -Value $XMLObj; @@ -33,10 +30,8 @@ while ($TRUE) { } catch { Write-IcingaEventMessage -EventId 1200 -Namespace 'Framework' -Objects $UpdateFile, $XMLObj, $_.Exception.Message; } finally { - $WindowsUpdates = $null; - $SearchIndex = $null; - $Pending = $null; - $XMLObj = $null; + $Updates = $null; + $XMLObj = $null; # Fetch Windows Updates every 10 minutes (600 seconds) Start-Sleep -Seconds 600; } diff --git a/lib/provider/updates/Get-IcingaWindowsUpdatePendingList.psm1 b/lib/provider/updates/Get-IcingaWindowsUpdatePendingList.psm1 index 3620c12e..c9a96b67 100644 --- a/lib/provider/updates/Get-IcingaWindowsUpdatePendingList.psm1 +++ b/lib/provider/updates/Get-IcingaWindowsUpdatePendingList.psm1 @@ -58,21 +58,16 @@ function Get-IcingaWindowsUpdatePendingList() } $WindowsUpdates = Read-IcingaFileSecure -File $UpdateFile; - $Pending = ([System.Management.Automation.PSSerializer]::Deserialize($WindowsUpdates)); + $Pending = [System.Management.Automation.PSSerializer]::Deserialize($WindowsUpdates); + if ($null -eq $Pending) { + $Pending = @(); + } $LastWriteTime = [System.IO.File]::GetLastWriteTimeUtc($UpdateFile); $PendingUpdates.Add('fetched_hr', $LastWriteTime.ToString("yyyy-MM-dd HH:mm:ss 'UTC'")); $PendingUpdates.Add('fetched', (Get-IcingaUnixTimeOffsetNow -UnixTime ([DateTimeOffset]$LastWriteTime).ToUnixTimeSeconds())); } else { # Fetch all informations about installed updates and add them - try { - $WindowsUpdates = New-Object -ComObject "Microsoft.Update.Session" -ErrorAction Stop; - $SearchIndex = $WindowsUpdates.CreateUpdateSearcher(); - # Get a list of current pending updates which are not yet installed on the system - $Pending = $SearchIndex.Search("IsInstalled=0"); - $Pending = $Pending.Updates; - } catch { - Exit-IcingaThrowException -ExceptionType 'Permission' -ExceptionThrown $IcingaExceptions.Permission.WindowsUpdate -Force; - } + $Pending = Get-IcingaWindowsUpdateRaw; $PendingUpdates.Add('fetched_hr', [DateTime]::UtcNow.ToString("yyyy-MM-dd HH:mm:ss 'UTC'")); $PendingUpdates.Add('fetched', 0); diff --git a/lib/provider/updates/Get-IcingaWindowsUpdateRaw.psm1 b/lib/provider/updates/Get-IcingaWindowsUpdateRaw.psm1 new file mode 100644 index 00000000..73d917f9 --- /dev/null +++ b/lib/provider/updates/Get-IcingaWindowsUpdateRaw.psm1 @@ -0,0 +1,132 @@ +<# +.SYNOPSIS + Queries the Windows Update COM object for raw update objects. +.DESCRIPTION + Directly queries the Windows Update Agent COM object ('Microsoft.Update.Session') + using the specified search criteria and converts the unmanaged COM results into + serializable PowerShell custom objects. + + This ensures that COM collections such as categories and KB article IDs are + properly converted into native arrays and can be safely serialized or used + directly by monitoring checks. +.FUNCTIONALITY + Fetches raw Windows Update objects via the Windows Update COM API. +.PARAMETER Criteria + The search criteria query string for the Windows Update searcher. + Defaults to 'IsInstalled=0'. +.OUTPUTS + [array] An array of [PSCustomObject] instances representing the found updates. +.EXAMPLE + PS>Get-IcingaWindowsUpdateRaw; +.EXAMPLE + PS>Get-IcingaWindowsUpdateRaw -Criteria 'IsInstalled=0 and Type="Software"'; +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Get-IcingaWindowsUpdateRaw() +{ + param ( + [string]$Criteria = 'IsInstalled=0' + ); + + [array]$UpdateList = @(); + + try { + $WindowsUpdates = New-Object -ComObject 'Microsoft.Update.Session' -ErrorAction Stop; + $SearchIndex = $WindowsUpdates.CreateUpdateSearcher(); + # Get a list of current pending updates which are not yet installed on the system + $Pending = $SearchIndex.Search($Criteria); + + foreach ($update in $Pending.Updates) { + [array]$categories = @(); + if ($null -ne $update.Categories) { + foreach ($category in $update.Categories) { + $categories += [PSCustomObject]@{ + 'Name' = $category.Name; + 'CategoryID' = $category.CategoryID; + 'Type' = $category.Type; + 'Description' = $category.Description; + }; + } + } + + [array]$kbArticleIDs = @(); + if ($null -ne $update.KBArticleIDs) { + $kbArticleIDs = @($update.KBArticleIDs); + } + [array]$securityBulletinIDs = @(); + if ($null -ne $update.SecurityBulletinIDs) { + $securityBulletinIDs = @($update.SecurityBulletinIDs); + } + [array]$supersededUpdateIDs = @(); + if ($null -ne $update.SupersededUpdateIDs) { + $supersededUpdateIDs = @($update.SupersededUpdateIDs); + } + [array]$cveIDs = @(); + if ($null -ne $update.CveIDs) { + $cveIDs = @($update.CveIDs); + } + [array]$languages = @(); + if ($null -ne $update.Languages) { + $languages = @($update.Languages); + } + [array]$moreInfoUrls = @(); + if ($null -ne $update.MoreInfoUrls) { + $moreInfoUrls = @($update.MoreInfoUrls); + } + [array]$uninstallationSteps = @(); + if ($null -ne $update.UninstallationSteps) { + $uninstallationSteps = @($update.UninstallationSteps); + } + + $UpdateList += [PSCustomObject]@{ + 'Title' = $update.Title; + 'Description' = $update.Description; + 'Categories' = $categories; + 'KBArticleIDs' = $kbArticleIDs; + 'SecurityBulletinIDs' = $securityBulletinIDs; + 'SupersededUpdateIDs' = $supersededUpdateIDs; + 'CveIDs' = $cveIDs; + 'Languages' = $languages; + 'MoreInfoUrls' = $moreInfoUrls; + 'Deadline' = $update.Deadline; + 'IsBeta' = $update.IsBeta; + 'IsDownloaded' = $update.IsDownloaded; + 'IsHidden' = $update.IsHidden; + 'IsInstalled' = $update.IsInstalled; + 'IsMandatory' = $update.IsMandatory; + 'IsUninstallable' = $update.IsUninstallable; + 'LastDeploymentChangeTime' = $update.LastDeploymentChangeTime; + 'MaxDownloadSize' = $update.MaxDownloadSize; + 'MinDownloadSize' = $update.MinDownloadSize; + 'MsrcSeverity' = $update.MsrcSeverity; + 'RecommendedCpuSpeed' = $update.RecommendedCpuSpeed; + 'RecommendedHardDiskSpace' = $update.RecommendedHardDiskSpace; + 'RecommendedMemory' = $update.RecommendedMemory; + 'ReleaseNotes' = $update.ReleaseNotes; + 'SupportUrl' = $update.SupportUrl; + 'Type' = $update.Type; + 'UninstallationNotes' = $update.UninstallationNotes; + 'UninstallationBehavior' = $update.UninstallationBehavior; + 'UninstallationSteps' = $uninstallationSteps; + 'DeploymentAction' = $update.DeploymentAction; + 'DownloadPriority' = $update.DownloadPriority; + 'RebootRequired' = $update.RebootRequired; + 'IsPresent' = $update.IsPresent; + 'BrowseOnly' = $update.BrowseOnly; + 'PerUser' = $update.PerUser; + 'AutoSelection' = $update.AutoSelection; + 'AutoDownload' = $update.AutoDownload; + }; + } + } catch { + Exit-IcingaThrowException -ExceptionType 'Permission' -ExceptionThrown $IcingaExceptions.Permission.WindowsUpdate -Force; + } finally { + $Pending = $null; + $SearchIndex = $null; + $WindowsUpdates = $null; + } + + return $UpdateList; +}