Files
linux-scripts/windows-updates-exporter.ps1
chiefgeek a1a17e81a1 Sync all scripts from website downloads — 352 scripts total
Includes updated JS challenge scripts with Claude-User whitelist,
same-site referer bypass, Blackbox-Exporter allowed bot, and all
new exporters, cheat sheets, and automation scripts.
2026-05-25 03:31:08 +02:00

561 lines
24 KiB
PowerShell

<#
.SYNOPSIS
Windows Update Monitoring Script for Prometheus Windows Exporter (wsus)
.DESCRIPTION
This script monitors Windows Updates and exports metrics in Prometheus format.
It checks for available updates, their severity levels, and installation status.
Can optionally install updates when specified.
.PARAMETER MetricNames
Custom metric names hashtable for overriding default metric names
.PARAMETER AutoInstall
Enable automatic installation of available updates
.PARAMETER ScheduleDaily
Create a scheduled task to run this script daily at 3 AM
.PARAMETER MetricsFilePath
Path where Prometheus metrics will be written (must be accessible by windows_exporter)
.NOTES
Version: 1.6.2
Author: Phil Connor contact@mylinux.work
License: MIT
Features:
- Monitors Windows Updates status
- Exports metrics in Prometheus format
- Supports automatic update installation
- Graceful shutdown support
- Retry mechanisms and timeout handling
- Memory management and garbage collection
- Comprehensive logging
- Error handling and metrics
#>
param(
# Custom metric names hashtable for overriding default metric names
[hashtable]$MetricNames,
# Enable automatic installation of available updates
[switch]$AutoInstall = $false,
# Create a scheduled task to run this script daily at 3 AM
[switch]$ScheduleDaily,
# Validate that the parent directory exists for the metrics file
[ValidateScript({Test-Path (Split-Path $_ -Parent) -PathType Container})]
# Path where Prometheus metrics will be written (must be accessible by windows_exporter)
[string]$MetricsFilePath = "C:\Program Files\windows_exporter\textfile_inputs\updates.prom"
)
# Check if script is already running to prevent multiple instances
$scriptName = $MyInvocation.MyCommand.Name
$currentProcess = Get-Process -Id $PID
$runningInstances = Get-WmiObject Win32_Process |
Where-Object { $_.CommandLine -like "*$scriptName*" -and $_.ProcessId -ne $currentProcess.Id }
if ($runningInstances) {
Write-Host "Script is already running (PID: $($runningInstances.ProcessId)). Exiting to prevent conflicts."
exit 0
}
# Create scheduled task for daily execution at 3 AM
if ($ScheduleDaily -eq $true) {
$taskName = "WindowsUpdateMonitoring"
# Check if the scheduled task already exists to avoid duplicates
$existingTask = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
if (-not $existingTask) {
# Define the action: run PowerShell with this script
$taskAction = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$($MyInvocation.MyCommand.Path)`""
# Set the trigger to run daily at 3 AM
$taskTrigger = New-ScheduledTaskTrigger -Daily -At 3AM
# Run as SYSTEM account with highest privileges for update operations
$taskPrincipal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
try {
Write-Host "Creating scheduled task: $taskName"
# Register the scheduled task with Windows Task Scheduler
Register-ScheduledTask -TaskName $taskName -Action $taskAction -Trigger $taskTrigger -Principal $taskPrincipal -Description "Monitors for Windows updates and optionally installs them automatically"
# Verify the task was created successfully
$createdTask = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
if (-not $createdTask) {
throw "Failed to verify scheduled task creation"
}
Write-Host "Successfully created scheduled task: $taskName"
} catch {
Write-Error "Failed to create auto-start task: $($_.Exception.Message)"
throw
}
} else {
Write-Host "Scheduled task $taskName already exists. Skipping creation."
}
}
# Define standard metric values used throughout the script
$script:MetricValues = @{
Success = 1 # Indicates successful operation
Error = 0 # Indicates error state
NoError = 0 # Indicates no error occurred
NoLastInstall = 0 # Indicates no previous installation timestamp
NoInstallStatus = 0 # Indicates no installation status available
}
# Define Prometheus metric names following naming conventions
# These metrics will be exposed to Prometheus for monitoring
Set-Variable -Name MetricNames -Value @{
# Script execution metrics - track the health and performance of this script
Status = "windows_update_script_status" # Status of the script execution (1 for running, 0 for not running)
Timestamp = "windows_update_script_timestamp_seconds" # Timestamp of the last script execution
Error = "windows_update_script_error" # Error message if script execution fails
Runtime = "windows_update_script_runtime_seconds" # Total runtime of the script
# Update information metrics - track Windows Update status and availability
Available = "windows_updates_available" # Total number of Windows updates available for installation
Info = "windows_update_info" # General update information
Reboot = "windows_update_reboot_required" # Reboot requirement flag
AutoUpdate = "windows_update_auto_install_enabled" # Automatic update installation enabled flag (0 or 1)
LastInstall = "windows_update_last_install_timestamp_seconds" # Timestamp of the last successful update installation
InstallStatus = "windows_update_install_status" # Status of the last update installation attempt (0 for success, 1 for failure)
UpdateList = "windows_update_available_list" # List of available updates with details
} -Option ReadOnly
# Validate all metric names conform to Prometheus naming standards
# Metric names must start with a letter or underscore, followed by letters, numbers, or underscores
$MetricNames.Values | ForEach-Object {
if (-not ($_ -match '^[a-zA-Z_:][a-zA-Z0-9_:]*$')) {
throw "Invalid Prometheus metric name: $_"
}
}
# Function to write errors to log file and create error metrics for monitoring
function Write-ScriptError {
param(
[string]$Message, # Error message to log
[string]$ErrorCode = "unknown", # Error code for categorization
[hashtable]$MetricNames, # Metric names hashtable
[string]$MetricsFilePath # Path to write error metrics
)
# Log the error to PowerShell error stream
Write-Error $Message
# Create a Prometheus error metric with sanitized error code
$errorMetric = "$($MetricNames.Error){error=`"$($ErrorCode -replace '"', '')`"} 1"
try {
# Write the error metric to the metrics file for Prometheus to collect
$errorMetric | Out-File -FilePath $MetricsFilePath -Encoding UTF8 -Force
}
catch {
Write-Warning "Failed to write error metric: $_"
}
}
# Function to execute actions with standardized error handling
function Invoke-WithErrorHandling {
param(
[scriptblock]$Action, # Script block to execute
[string]$Operation, # Description of the operation for error messages
[hashtable]$MetricNames, # Metric names hashtable
[string]$MetricsFilePath # Path to write error metrics
)
try {
# Execute the provided script block
& $Action
} catch {
# Handle any errors by writing to metrics and re-throwing
Write-ScriptError -Message "Failed to $Operation`: $_" -ErrorCode $Operation.Replace(' ', '_') -MetricNames $MetricNames -MetricsFilePath $MetricsFilePath
throw
}
}
# Function to download and install Windows updates
function Install-WindowsUpdate {
param(
[Parameter(Mandatory)]
[ValidateNotNull()]
$Update, # Windows Update object to install
[Parameter(Mandatory)]
[ValidateNotNull()]
$UpdateSession, # Windows Update session object
[Parameter(Mandatory)]
[ValidateNotNull()]
[hashtable]$MetricNames, # Metric names hashtable
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$SanitizedTitle # Sanitized update title for metrics
)
# Track COM objects for proper cleanup
$comObjects = @()
try {
Write-Information "Installing update: $($Update.Title)" -InformationAction Continue
# Create and configure the update downloader
$UpdateDownloader = $UpdateSession.CreateUpdateDownloader()
$comObjects += $UpdateDownloader
$UpdateDownloader.Updates = New-Object -ComObject Microsoft.Update.UpdateColl
$comObjects += $UpdateDownloader.Updates
$UpdateDownloader.Updates.Add($Update) # Add the update to the collection
$UpdateDownloader.Download() # Download the update files
# Create and configure the update installer
$UpdateInstaller = $UpdateSession.CreateUpdateInstaller()
$UpdateInstaller.Updates = $UpdateDownloader.Updates
$InstallResult = $UpdateInstaller.Install() # Install the downloaded update
# Generate Unix timestamp for the installation
$installTimestamp = [Math]::Floor([decimal](Get-Date(Get-Date).ToUniversalTime()-uformat '%s'))
# Create Prometheus metrics for the installation
$additionalMetrics = @()
$lastMetric = "# HELP $($MetricNames.LastInstall) Unix timestamp when updates were last installed`n# TYPE $($MetricNames.LastInstall) gauge"
$additionalMetrics += $lastMetric
$additionalMetrics += "$($MetricNames.LastInstall){update=`"$SanitizedTitle`"} $installTimestamp"
$installMetric = "# HELP $($MetricNames.InstallStatus) Status of update installation`n# TYPE $($MetricNames.InstallStatus) gauge"
$additionalMetrics += $installMetric
$additionalMetrics += "$($MetricNames.InstallStatus){update=`"$SanitizedTitle`",result=`"$($InstallResult.ResultCode)`"} 1"
Write-Verbose "Installation completed with result: $($InstallResult.ResultCode)"
# Return success result object
return [PSCustomObject]@{
Success = $InstallResult.ResultCode -eq 2 # ResultCode 2 = successful installation
ResultCode = $InstallResult.ResultCode
UpdateTitle = $Update.Title
InstallTimestamp = $installTimestamp
ErrorMessage = $null
Metrics = $additionalMetrics
}
}
catch {
$errorMessage = $_.Exception.Message
Write-Host "Error installing update: $_"
# Return error result object
return [PSCustomObject]@{
Success = $false
ResultCode = -1
UpdateTitle = $Update.Title
InstallTimestamp = $null
ErrorMessage = $errorMessage
Metrics = @()
}
}
finally {
# Clean up COM objects to prevent memory leaks
$comObjects | ForEach-Object {
if ($_ -ne $null) {
try {
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($_) | Out-Null
} catch {
Write-Warning "Failed to release COM object: $_"
}
}
}
}
}
# Function to check if a system reboot is required after updates
function Get-RebootStatus {
try {
# Check if Windows OS indicates a reboot is pending
$osRebootPending = (Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop).RebootPending
# Check Component Based Servicing registry key for pending operations
$cbsRebootPending = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing' -Name 'RebootPending' -ErrorAction Stop
# Return 1 if either check indicates reboot required, 0 otherwise
return [int]($osRebootPending -or $cbsRebootPending)
}
catch {
Write-Warning "Could not determine reboot status: $($_.Exception.Message)"
return -1 # Return -1 to indicate indeterminate status
}
}
# Check current reboot status for inclusion in metrics
$rebootRequired = Get-RebootStatus
# Function to sanitize strings for safe use as Prometheus labels
function ConvertTo-SafePrometheusLabel {
param(
[Parameter(Mandatory=$false)]
[AllowEmptyString()]
[string]$Value # String value to sanitize
)
# Return empty string if input is null or empty
if ([string]::IsNullOrEmpty($Value)) {
return ""
}
# Sanitize special characters that could break Prometheus format
# Replace backslashes, quotes, newlines, tabs, and non-printable characters
$sanitized = $Value -replace '[\\"\r\n\t]|[^\x20-\x7E]', {
switch ($_.Value) {
'\' { '\\' } # Escape backslashes
'"' { '\"' } # Escape double quotes
default { if ($_.Value -match '[\r\n\t]') { ' ' } else { '_' } } # Replace whitespace/unprintable with underscore
}
}
# Limit length to prevent excessively long labels (Prometheus best practice)
return $sanitized.Substring(0, [Math]::Min($sanitized.Length, 256))
}
# Function to build Prometheus metric strings and add them to StringBuilder
function Add-PrometheusMetric {
param(
[Parameter(Mandatory)]
[System.Text.StringBuilder]$StringBuilder, # StringBuilder to append metrics to
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$MetricName, # Name of the Prometheus metric
[Parameter(Mandatory)]
[string]$HelpText, # Help text describing the metric
[Parameter(Mandatory)]
$Value, # Metric value
[hashtable]$Labels = @{} # Optional labels for the metric
)
try {
# Add Prometheus HELP comment explaining what the metric measures
$StringBuilder.AppendLine("# HELP $MetricName $HelpText") | Out-Null
# Add Prometheus TYPE comment (assuming gauge type for all metrics)
$StringBuilder.AppendLine("# TYPE $MetricName gauge") | Out-Null
if ($null -ne $Value) {
# Build label string if labels are provided
$labelString = ""
if ($Labels.Count -gt 0) {
$labelPairs = $Labels.GetEnumerator() | ForEach-Object {
"$($_.Key)=`"$($_.Value)`""
}
$labelString = "{$($labelPairs -join ',')}"
}
# Add the actual metric line with name, labels, and value
$StringBuilder.AppendLine("$MetricName$labelString $Value") | Out-Null
}
}
catch {
Write-Error "Failed to add Prometheus metric '$MetricName': $_"
throw
}
}
# Function to generate complete Prometheus metrics output
function Get-PrometheusMetrics {
param(
[int]$UpdateCount, # Number of available updates
[double]$ScriptRunTime, # Time taken to run the script
[bool]$rebootRequired, # Whether a system reboot is required
[bool]$AutoUpdateEnabled, # Whether automatic updates are enabled
[array]$AvailableUpdates = @() # Array of available update details
)
# Validate input parameters
if ($UpdateCount -lt 0) {
throw "UpdateCount must be non-negative"
}
if ($ScriptRunTime -lt 0) {
throw "ScriptRunTime must be non-negative"
}
try {
# Get current Unix timestamp for the script execution time
$currentTimestamp = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
} catch {
Write-Error "Failed to initialize metrics generation: $_"
return $null
}
# Convert boolean values to integers for Prometheus (0 or 1)
$rebootValue = [int]$rebootRequired
$autoUpdateValue = [int]$AutoUpdateEnabled
# Define all metrics to be generated with their properties
$metrics = @(
@{ Name = $MetricNames.Available; Help = "Number of Windows updates available from WSUS"; Value = $UpdateCount }
@{ Name = $MetricNames.Status; Help = "Indicates if the update script has run successfully"; Value = 1; Labels = @{status="success"} }
@{ Name = $MetricNames.Timestamp; Help = "Unix timestamp when the script last ran"; Value = $currentTimestamp }
@{ Name = $MetricNames.Error; Help = "Information about script errors"; Value = 0 }
@{ Name = $MetricNames.Reboot; Help = "Indicates if a reboot is required after updates"; Value = $rebootValue }
@{ Name = $MetricNames.Runtime; Help = "Time taken to execute the update script in seconds"; Value = $ScriptRunTime }
@{ Name = $MetricNames.AutoUpdate; Help = "Indicates if automatic update installation is enabled"; Value = $autoUpdateValue }
@{ Name = $MetricNames.LastInstall; Help = "Unix timestamp when updates were last installed"; Value = 0 }
@{ Name = $MetricNames.InstallStatus; Help = "Status of update installation"; Value = 0 }
)
# Build the complete metrics string
$metricsBuilder = [System.Text.StringBuilder]::new()
foreach ($metric in $metrics) {
try {
Add-PrometheusMetric -StringBuilder $metricsBuilder -MetricName $metric.Name -HelpText $metric.Help -Value $metric.Value -Labels $metric.Labels
} catch {
Write-Warning "Failed to add metric '$($metric.Name)': $_"
}
}
# Add individual update metrics - one per available update
if ($AvailableUpdates.Count -gt 0) {
$metricsBuilder.AppendLine("# HELP $($MetricNames.UpdateList) Individual Windows updates available for installation") | Out-Null
$metricsBuilder.AppendLine("# TYPE $($MetricNames.UpdateList) gauge") | Out-Null
foreach ($update in $AvailableUpdates) {
$labels = @{
title = $update.Title
severity = $update.Severity
downloaded = $update.IsDownloaded
size_bytes = $update.Size
}
$labelString = ($labels.GetEnumerator() | ForEach-Object { "$($_.Key)=`"$($_.Value)`"" }) -join ','
$metricsBuilder.AppendLine("$($MetricNames.UpdateList){$labelString} 1") | Out-Null
}
}
return $metricsBuilder.ToString()
}
# Validate that metric names configuration is available
if (-not $MetricNames) {
Write-Error "MetricNames configuration is not available"
return $null
}
# Graceful shutdown flag
$global:shutdown = $false
# Register event handler for graceful shutdown
Register-EngineEvent -SourceIdentifier "PowerShell.Exiting" -Action {
Write-Host "Initiating graceful shutdown..."
$global:shutdown = $true
}
# Trap Ctrl+C and other termination signals
[Console]::TreatControlCAsInput = $false
Register-ObjectEvent -InputObject ([Console]) -EventName CancelKeyPress -Action {
param($Sender, $CancelEventArgs)
Write-Host "Shutdown signal received. Cleaning up..."
$CancelEventArgs.Cancel = $true
$global:shutdown = $true
}
# Function to check for shutdown signal during long operations
function Test-ShutdownSignal {
if ($global:shutdown) {
Write-Host "Shutdown requested. Exiting gracefully..."
exit 0
}
}
# Main execution: Check for Windows Updates from WSUS
Write-Host "Checking for Windows Updates from WSUS..."
$StartTime = Get-Date
$comObjects = @()
$availableUpdates = @()
try {
# Check for shutdown before starting
Test-ShutdownSignal
# Create Windows Update session and searcher COM objects
$UpdateSession = New-Object -ComObject Microsoft.Update.Session
$comObjects += $UpdateSession
$UpdateSearcher = $UpdateSession.CreateUpdateSearcher()
$comObjects += $UpdateSearcher
# Check for shutdown before search
Test-ShutdownSignal
# Search for updates that are not yet installed
$SearchResult = $UpdateSearcher.Search("IsInstalled=0")
# Process individual updates for detailed metrics
foreach ($Update in $SearchResult.Updates) {
$sanitizedTitle = ConvertTo-SafePrometheusLabel -Value $Update.Title
$sanitizedDescription = ConvertTo-SafePrometheusLabel -Value $Update.Description
$updateSize = if ($Update.MaxDownloadSize) { $Update.MaxDownloadSize } else { 0 }
$availableUpdates += [PSCustomObject]@{
Title = $sanitizedTitle
Description = $sanitizedDescription
Size = $updateSize
Severity = if ($Update.MsrcSeverity) { $Update.MsrcSeverity } else { "Unknown" }
IsDownloaded = [int]$Update.IsDownloaded
}
}
} catch {
# Handle connection failures and write error metric
Write-Error "Failed to connect to Windows Update service: $_"
$errorMetric = "$($MetricNames.Error){error=`"wsus_connection_failed`"} 1"
$errorMetric | Out-File -FilePath $MetricsFilePath -Encoding UTF8 -Force
exit 1
} finally {
# Clean up COM objects to prevent memory leaks
$comObjects | ForEach-Object {
if ($_ -and [System.Runtime.InteropServices.Marshal]::IsComObject($_)) {
try {
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($_) | Out-Null
} catch {
Write-Warning "Failed to release COM object: $_"
}
}
}
}
# Calculate script execution time and generate base metrics
$EndTime = Get-Date
$ScriptRunTime = ($EndTime - $StartTime).TotalSeconds
# Safely get update count even if SearchResult is null
$updateCount = if ($SearchResult -and $SearchResult.Updates) { $SearchResult.Updates.Count } else { 0 }
$prometheusMetric = Get-PrometheusMetrics -UpdateCount $updateCount -ScriptRunTime $ScriptRunTime -AutoUpdateEnabled $AutoInstall -AvailableUpdates $availableUpdates
# Initialize array for additional metrics from update installations
$additionalMetrics = @()
# Process the search results
if ($SearchResult.Updates.Count -eq 0) {
Write-Host "No updates available from WSUS."
} else {
Write-Host "Found $($SearchResult.Updates.Count) update(s) available:"
$updateList = [System.Collections.ArrayList]::new()
# Iterate through each available update
foreach ($Update in $SearchResult.Updates) {
# Check for shutdown signal between updates
Test-ShutdownSignal
Write-Host "- $($Update.Title)"
$sanitizedTitle = ConvertTo-SafePrometheusLabel -Value $Update.Title
[void]$updateList.Add("title=`"$sanitizedTitle`"")
# Install the update if AutoInstall is enabled
if ($AutoInstall) {
# Check for shutdown before installing
Test-ShutdownSignal
$result = Install-WindowsUpdate -Update $Update -UpdateSession $UpdateSession -MetricNames $MetricNames -SanitizedTitle $sanitizedTitle
if ($result.Metrics) {
$additionalMetrics += $result.Metrics
}
}
}
}
# Write all metrics to the output file for Prometheus collection
try {
# Final shutdown check before writing metrics
Test-ShutdownSignal
$allMetrics = @($prometheusMetric) + $additionalMetrics
($allMetrics -join "`n") | Out-File -FilePath "$MetricsFilePath" -Encoding UTF8 -Force
Write-Host "Metrics successfully written to $MetricsFilePath"
} catch {
# Handle file write errors and create error metric
Write-Error "Failed to write metrics to file: $_"
$errorCode = $_.Exception.HResult -replace '"', '\"'
"$($MetricNames.Error){error=`"file_write_failed`",error_code=`"$errorCode`"} 1" | Out-File -FilePath "$MetricsFilePath" -Encoding UTF8 -Force
}
Write-Host "Script completed successfully."