Scripts/Update-OpenEndpointEventsConfig.ps1
|
<#
.SYNOPSIS Refreshes the local OpenEndpointEvents uploader configuration from a protected HTTPS config blob. .DESCRIPTION Downloads a remote uploader-config.json file, validates the content, writes it locally, locks down ACLs, updates refresh state, and optionally applies scheduled task settings from the remote config. .PARAMETER RefreshConfigPath Local config-refresh.json path. .PARAMETER Force Bypasses MinimumRefreshIntervalHours. .PARAMETER ApplySchedule Applies scheduled task configuration from the downloaded remote uploader config. .PARAMETER StartUploaderAfterUpdate Starts the upload task after successful config refresh. .PARAMETER CorrelationId Optional shared correlation ID for installer/config/uploader event correlation. .EXAMPLE .\Update-OpenEndpointEventsConfig.ps1 -Force -ApplySchedule -Verbose #> [CmdletBinding()] param( [string]$RefreshConfigPath = "C:\ProgramData\OpenEndpointEvents\Config\config-refresh.json", [switch]$Force, [switch]$ApplySchedule, [switch]$StartUploaderAfterUpdate, [string]$CorrelationId ) $ErrorActionPreference = "Stop" $script:OpenEndpointEventsModuleImported = $false function Import-OpenEndpointEventsQuiet { if ($script:OpenEndpointEventsModuleImported) { return } if (Get-Command Write-EndpointInfo -ErrorAction SilentlyContinue) { $script:OpenEndpointEventsModuleImported = $true return } $module = Get-Module -ListAvailable OpenEndpointEvents | Sort-Object Version -Descending | Select-Object -First 1 if ($module) { Import-Module $module.Path ` -Force ` -Global ` -ErrorAction SilentlyContinue ` -Verbose:$false | Out-Null $script:OpenEndpointEventsModuleImported = $true } } if ([string]::IsNullOrWhiteSpace($CorrelationId)) { $CorrelationId = "CONFIG-$((Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssZ'))-$($env:COMPUTERNAME)-$(([guid]::NewGuid().ToString()).Substring(0,8))" } $BaseRoot = "C:\ProgramData\OpenEndpointEvents" $ConfigRoot = Join-Path $BaseRoot "Config" $StateRoot = Join-Path $BaseRoot "State" $ScriptRoot = Join-Path $BaseRoot "Scripts" $UploadScriptPath = Join-Path $ScriptRoot "Upload-EndpointEvents.ps1" $ConfigRefreshScriptPath = Join-Path $ScriptRoot "Update-OpenEndpointEventsConfig.ps1" $ConfigTaskName = "OpenEndpointEvents Config Refresh" $UploadTaskName = "OpenEndpointEvents Upload" function Set-OpenEndpointEventsConfigAcl { param( [string]$Path ) if (-not (Test-Path -Path $Path)) { New-Item -Path $Path -ItemType Directory -Force | Out-Null } icacls $Path /inheritance:r | Out-Null icacls $Path /grant:r "SYSTEM:(OI)(CI)(F)" | Out-Null icacls $Path /grant:r "Administrators:(OI)(CI)(F)" | Out-Null icacls $Path /remove "Users" "Authenticated Users" "Everyone" 2>$null | Out-Null } function Write-StepInfo { param( [string]$EventName, [string]$Message, [hashtable]$Data ) Write-Verbose "[OpenEndpointEventsConfig] $Message" Import-OpenEndpointEventsQuiet if (Get-Command Write-EndpointInfo -ErrorAction SilentlyContinue) { if ($null -eq $Data) { $Data = @{} } $Data["StepStatus"] = "Success" Write-EndpointInfo ` -Source "OpenEndpointEventsConfig" ` -EventName $EventName ` -Message $Message ` -CorrelationId $CorrelationId ` -IncludeEndpointIdentity ` -IncludeProcessInfo ` -Data $Data | Out-Null } } function Write-StepWarn { param( [string]$EventName, [string]$Message, [hashtable]$Data ) Write-Verbose "[OpenEndpointEventsConfig] WARNING: $Message" Import-OpenEndpointEventsQuiet if (Get-Command Write-EndpointWarn -ErrorAction SilentlyContinue) { if ($null -eq $Data) { $Data = @{} } $Data["StepStatus"] = "Warning" Write-EndpointWarn ` -Source "OpenEndpointEventsConfig" ` -EventName $EventName ` -Message $Message ` -CorrelationId $CorrelationId ` -IncludeEndpointIdentity ` -IncludeProcessInfo ` -Data $Data | Out-Null } } function Write-StepError { param( [string]$EventName, [string]$Message, [System.Management.Automation.ErrorRecord]$ErrorRecord, [hashtable]$Data ) Write-Verbose "[OpenEndpointEventsConfig] ERROR: $Message" Import-OpenEndpointEventsQuiet if (Get-Command Write-EndpointError -ErrorAction SilentlyContinue) { if ($null -eq $Data) { $Data = @{} } $Data["StepStatus"] = "Failed" Write-EndpointError ` -Source "OpenEndpointEventsConfig" ` -EventName $EventName ` -Message $Message ` -CorrelationId $CorrelationId ` -ErrorRecord $ErrorRecord ` -IncludeEndpointIdentity ` -IncludeProcessInfo ` -Data $Data | Out-Null } } function New-DailyIntervalTriggers { param( [int]$IntervalMinutes, [switch]$IncludeStartup, [switch]$IncludeLogon ) if ($IntervalMinutes -lt 15 -or $IntervalMinutes -gt 1440) { throw "IntervalMinutes must be between 15 and 1440." } $triggers = New-Object System.Collections.Generic.List[object] if ($IncludeStartup) { $triggers.Add((New-ScheduledTaskTrigger -AtStartup)) } if ($IncludeLogon) { $triggers.Add((New-ScheduledTaskTrigger -AtLogOn)) } $minute = 0 while ($minute -lt 1440) { $time = [datetime]::Today.AddMinutes($minute) $triggers.Add((New-ScheduledTaskTrigger -Daily -At $time)) $minute += $IntervalMinutes } return $triggers.ToArray() } function Register-OpenEndpointEventsTask { param( [string]$TaskName, [string]$ScriptPath, [string[]]$ScriptArguments, [int]$IntervalMinutes ) if (-not (Test-Path -Path $ScriptPath)) { throw "Task script path does not exist: $ScriptPath" } $argumentText = "-NoProfile -ExecutionPolicy Bypass -File `"$ScriptPath`"" if ($ScriptArguments -and $ScriptArguments.Count -gt 0) { $argumentText = "$argumentText $($ScriptArguments -join ' ')" } $action = New-ScheduledTaskAction ` -Execute "powershell.exe" ` -Argument $argumentText $triggers = New-DailyIntervalTriggers ` -IntervalMinutes $IntervalMinutes ` -IncludeStartup ` -IncludeLogon $principal = New-ScheduledTaskPrincipal ` -UserId "SYSTEM" ` -RunLevel Highest $settings = New-ScheduledTaskSettingsSet ` -AllowStartIfOnBatteries ` -DontStopIfGoingOnBatteries ` -StartWhenAvailable ` -MultipleInstances IgnoreNew ` -ExecutionTimeLimit (New-TimeSpan -Minutes 10) Register-ScheduledTask ` -TaskName $TaskName ` -Action $action ` -Trigger $triggers ` -Principal $principal ` -Settings $settings ` -Force | Out-Null } function Apply-OpenEndpointEventsSchedule { param( [object]$Config ) $uploadIntervalMinutes = if ($Config.UploadIntervalMinutes -ne $null) { [int]$Config.UploadIntervalMinutes } elseif ($Config.MinimumUploadIntervalMinutes -ne $null) { [int]$Config.MinimumUploadIntervalMinutes } else { 60 } $configRefreshIntervalHours = if ($Config.ConfigRefreshIntervalHours -ne $null) { [int]$Config.ConfigRefreshIntervalHours } else { 6 } if ($uploadIntervalMinutes -lt 15 -or $uploadIntervalMinutes -gt 1440) { throw "UploadIntervalMinutes must be between 15 and 1440." } if ($configRefreshIntervalHours -lt 1 -or $configRefreshIntervalHours -gt 24) { throw "ConfigRefreshIntervalHours must be between 1 and 24." } $configRefreshIntervalMinutes = $configRefreshIntervalHours * 60 Write-StepInfo ` -EventName "ScheduleApplyStarted" ` -Message "Applying scheduled task configuration from remote config" ` -Data @{ UploadIntervalMinutes = $uploadIntervalMinutes ConfigRefreshIntervalHours = $configRefreshIntervalHours ConfigRefreshIntervalMins = $configRefreshIntervalMinutes UploadTaskName = $UploadTaskName ConfigTaskName = $ConfigTaskName } Register-OpenEndpointEventsTask ` -TaskName $ConfigTaskName ` -ScriptPath $ConfigRefreshScriptPath ` -ScriptArguments @("-ApplySchedule") ` -IntervalMinutes $configRefreshIntervalMinutes Register-OpenEndpointEventsTask ` -TaskName $UploadTaskName ` -ScriptPath $UploadScriptPath ` -ScriptArguments @("-Window", "AllChanged") ` -IntervalMinutes $uploadIntervalMinutes Write-StepInfo ` -EventName "ScheduleApplied" ` -Message "Scheduled task configuration applied from remote config" ` -Data @{ UploadIntervalMinutes = $uploadIntervalMinutes ConfigRefreshIntervalHours = $configRefreshIntervalHours ConfigRefreshIntervalMins = $configRefreshIntervalMinutes UploadTaskName = $UploadTaskName ConfigTaskName = $ConfigTaskName Status = "Success" } } try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 Write-Verbose "[OpenEndpointEventsConfig] CorrelationId: $CorrelationId" Write-StepInfo ` -EventName "ConfigRefreshStarted" ` -Message "OpenEndpointEvents config refresh started" ` -Data @{ RefreshConfigPath = $RefreshConfigPath Force = [bool]$Force ApplySchedule = [bool]$ApplySchedule } if (-not (Test-Path -Path $RefreshConfigPath)) { throw "Refresh config file not found: $RefreshConfigPath" } $refreshConfig = Get-Content -Path $RefreshConfigPath -Raw | ConvertFrom-Json $configUri = [string]$refreshConfig.ConfigUri $localConfigPath = [string]$refreshConfig.LocalConfigPath $statePath = [string]$refreshConfig.StatePath if ([string]::IsNullOrWhiteSpace($configUri)) { throw "ConfigUri is missing from $RefreshConfigPath" } if ($configUri -notmatch "^https://") { throw "ConfigUri must use HTTPS." } if ([string]::IsNullOrWhiteSpace($localConfigPath)) { $localConfigPath = "C:\ProgramData\OpenEndpointEvents\Config\uploader-config.json" } if ([string]::IsNullOrWhiteSpace($statePath)) { $statePath = "C:\ProgramData\OpenEndpointEvents\State\config-refresh-state.json" } $localConfigRoot = Split-Path -Path $localConfigPath -Parent $stateRoot = Split-Path -Path $statePath -Parent New-Item -Path $localConfigRoot -ItemType Directory -Force | Out-Null New-Item -Path $stateRoot -ItemType Directory -Force | Out-Null Set-OpenEndpointEventsConfigAcl -Path $localConfigRoot $state = $null if (Test-Path -Path $statePath) { $state = Get-Content -Path $statePath -Raw | ConvertFrom-Json } $minimumRefreshIntervalHours = 6 if ($refreshConfig.MinimumRefreshIntervalHours) { $minimumRefreshIntervalHours = [int]$refreshConfig.MinimumRefreshIntervalHours } elseif ($state -and $state.ConfigRefreshIntervalHours) { $minimumRefreshIntervalHours = [int]$state.ConfigRefreshIntervalHours } if (-not $Force -and $state -and $state.LastRefreshUtc -and $minimumRefreshIntervalHours -gt 0) { $lastRefresh = [datetime]$state.LastRefreshUtc $nextAllowed = $lastRefresh.ToUniversalTime().AddHours($minimumRefreshIntervalHours) if ((Get-Date).ToUniversalTime() -lt $nextAllowed) { Write-StepInfo ` -EventName "ConfigRefreshSkipped" ` -Message "OpenEndpointEvents config refresh skipped due to minimum refresh interval" ` -Data @{ RefreshConfigPath = $RefreshConfigPath LocalConfigPath = $localConfigPath LastRefreshUtc = $lastRefresh.ToUniversalTime().ToString("o") NextAllowedUtc = $nextAllowed.ToString("o") MinimumRefreshIntervalHours = $minimumRefreshIntervalHours Status = "Skipped" } exit 0 } } $tempPath = Join-Path $env:TEMP "uploader-config-$([guid]::NewGuid()).json" try { Write-StepInfo ` -EventName "RemoteConfigDownloadStarted" ` -Message "Downloading remote uploader config" ` -Data @{ ConfigUriConfigured = $true TempPath = $tempPath } $response = Invoke-WebRequest ` -Uri $configUri ` -OutFile $tempPath ` -UseBasicParsing ` -ErrorAction Stop Write-StepInfo ` -EventName "RemoteConfigDownloaded" ` -Message "Remote uploader config downloaded" ` -Data @{ StatusCode = [int]$response.StatusCode TempPath = $tempPath } $downloadedConfigRaw = Get-Content -Path $tempPath -Raw $downloadedConfig = $downloadedConfigRaw | ConvertFrom-Json Write-StepInfo ` -EventName "RemoteConfigValidationStarted" ` -Message "Validating remote uploader config" ` -Data @{ ConfigVersion = $downloadedConfig.ConfigVersion } if ([string]::IsNullOrWhiteSpace($downloadedConfig.ContainerSasUrl)) { throw "Downloaded uploader config does not contain ContainerSasUrl." } if ($downloadedConfig.ContainerSasUrl -notmatch "^https://") { throw "ContainerSasUrl must use HTTPS." } if ($downloadedConfig.ContainerSasUrl -notmatch "\?") { throw "ContainerSasUrl does not contain a SAS query string." } if ([string]::IsNullOrWhiteSpace($downloadedConfig.LogRoot)) { throw "Downloaded uploader config does not contain LogRoot." } if ([string]::IsNullOrWhiteSpace($downloadedConfig.StateRoot)) { throw "Downloaded uploader config does not contain StateRoot." } if ([string]::IsNullOrWhiteSpace($downloadedConfig.BlobPrefix)) { throw "Downloaded uploader config does not contain BlobPrefix." } if ($downloadedConfig.UploadSasExpiresUtc) { $expiry = [datetime]$downloadedConfig.UploadSasExpiresUtc if ($expiry.ToUniversalTime() -lt (Get-Date).ToUniversalTime()) { throw "Downloaded upload SAS is already expired. UploadSasExpiresUtc=$($expiry.ToUniversalTime().ToString('o'))" } } $uploadIntervalMinutes = if ($downloadedConfig.UploadIntervalMinutes -ne $null) { [int]$downloadedConfig.UploadIntervalMinutes } else { 60 } $configRefreshIntervalHours = if ($downloadedConfig.ConfigRefreshIntervalHours -ne $null) { [int]$downloadedConfig.ConfigRefreshIntervalHours } else { 6 } Write-StepInfo ` -EventName "RemoteConfigValidated" ` -Message "Remote uploader config validated" ` -Data @{ ConfigVersion = $downloadedConfig.ConfigVersion UploadSasExpiresUtc = $downloadedConfig.UploadSasExpiresUtc UploadIntervalMinutes = $uploadIntervalMinutes ConfigRefreshIntervalHours = $configRefreshIntervalHours BlobStoreGroupBy = ($downloadedConfig.BlobStoreGroupBy -join ",") BlobWriteMode = $downloadedConfig.BlobWriteMode } Copy-Item -Path $tempPath -Destination $localConfigPath -Force Set-OpenEndpointEventsConfigAcl -Path $localConfigRoot $hash = (Get-FileHash -Path $localConfigPath -Algorithm SHA256).Hash Write-StepInfo ` -EventName "LocalConfigWritten" ` -Message "Local uploader config written" ` -Data @{ LocalConfigPath = $localConfigPath Sha256 = $hash } # Update config-refresh.json with current refresh interval from remote config. $updatedRefreshConfig = [ordered]@{ ConfigUri = $configUri LocalConfigPath = $localConfigPath StatePath = $statePath MinimumRefreshIntervalHours = $configRefreshIntervalHours RestartUploadTaskAfterUpdate = [bool]$refreshConfig.RestartUploadTaskAfterUpdate } $updatedRefreshConfig | ConvertTo-Json -Depth 10 | Set-Content -Path $RefreshConfigPath -Encoding UTF8 Set-OpenEndpointEventsConfigAcl -Path $localConfigRoot $newState = [ordered]@{ LastRefreshUtc = (Get-Date).ToUniversalTime().ToString("o") LocalConfigPath = $localConfigPath ConfigVersion = $downloadedConfig.ConfigVersion UploadSasExpiresUtc = $downloadedConfig.UploadSasExpiresUtc UploadIntervalMinutes = $uploadIntervalMinutes ConfigRefreshIntervalHours = $configRefreshIntervalHours Sha256 = $hash StatusCode = [int]$response.StatusCode Status = "Success" } $newState | ConvertTo-Json -Depth 10 | Set-Content -Path $statePath -Encoding UTF8 Write-StepInfo ` -EventName "ConfigStateWritten" ` -Message "Config refresh state written" ` -Data @{ StatePath = $statePath ConfigVersion = $downloadedConfig.ConfigVersion UploadIntervalMinutes = $uploadIntervalMinutes ConfigRefreshIntervalHours = $configRefreshIntervalHours Status = "Success" } if ($ApplySchedule) { Apply-OpenEndpointEventsSchedule -Config $downloadedConfig } Write-StepInfo ` -EventName "ConfigRefreshCompleted" ` -Message "OpenEndpointEvents uploader config refresh completed" ` -Data @{ LocalConfigPath = $localConfigPath ConfigVersion = $downloadedConfig.ConfigVersion UploadIntervalMinutes = $uploadIntervalMinutes ConfigRefreshIntervalHours = $configRefreshIntervalHours ApplySchedule = [bool]$ApplySchedule Status = "Success" } if ($StartUploaderAfterUpdate -or [bool]$updatedRefreshConfig.RestartUploadTaskAfterUpdate) { Write-StepInfo ` -EventName "StartUploaderRequested" ` -Message "Starting uploader after config update" ` -Data @{ UploadTaskName = $UploadTaskName } Start-ScheduledTask -TaskName $UploadTaskName -ErrorAction SilentlyContinue } } finally { Remove-Item -Path $tempPath -Force -ErrorAction SilentlyContinue } exit 0 } catch { Write-StepError ` -EventName "ConfigUpdateFailed" ` -Message "OpenEndpointEvents uploader config update failed" ` -ErrorRecord $_ ` -Data @{ RefreshConfigPath = $RefreshConfigPath ApplySchedule = [bool]$ApplySchedule Status = "Failed" } exit 1 } |