param( [string]$ManifestUrl = "", [string]$Channel = "stable", [string]$TempRoot = "$env:TEMP\OCSentinelUpdate", [switch]$Force ) $ErrorActionPreference = "Stop" function Initialize-OCSentinelTls { $protocols = [Net.SecurityProtocolType]::Tls12 if ([Enum]::GetNames([Net.SecurityProtocolType]) -contains "Tls13") { $protocols = $protocols -bor [Net.SecurityProtocolType]::Tls13 } [Net.ServicePointManager]::SecurityProtocol = $protocols [Net.ServicePointManager]::Expect100Continue = $false } function Get-OCSentinelManifest { param([Parameter(Mandatory)][string]$Uri) $parameters = @{ Method = "Get"; Uri = $Uri; TimeoutSec = 60 } if ((Get-Command Invoke-RestMethod).Parameters.ContainsKey("UseBasicParsing")) { $parameters.UseBasicParsing = $true } for ($attempt = 1; $attempt -le 3; $attempt++) { try { return Invoke-RestMethod @parameters } catch { if ($attempt -eq 3) { throw "Could not retrieve the OCSentinel release manifest after 3 attempts. Verify that the device can reach gitea.officecom.cloud with TLS 1.2 or newer. Last error: $($_.Exception.Message)" } Start-Sleep -Seconds (3 * $attempt) } } } Initialize-OCSentinelTls $installRoot = Join-Path ${env:ProgramFiles} "OCSentinel" $appExe = Join-Path $installRoot "app\OCSentinelCli.exe" $installScript = Join-Path $installRoot "scripts\install-ocsentinel.ps1" $versionFile = Join-Path $installRoot "VERSION.txt" function Get-InstalledVersion { if (Test-Path $versionFile) { return (Get-Content $versionFile -Raw).Trim() } if (Test-Path $appExe) { return (Get-Item $appExe).VersionInfo.ProductVersion } return "0.0.0" } function Compare-Version { param( [Parameter(Mandatory)][string]$Left, [Parameter(Mandatory)][string]$Right ) try { $leftVersion = [System.Version]$Left $rightVersion = [System.Version]$Right return $leftVersion.CompareTo($rightVersion) } catch { return [string]::Compare($Left, $Right, $true) } } function Get-Sha256Hex { param([Parameter(Mandatory)][string]$Path) return (Get-FileHash -Path $Path -Algorithm SHA256).Hash.ToLowerInvariant() } function Resolve-ManifestUrl { param( [Parameter(Mandatory)][string]$ManifestUrl, [Parameter(Mandatory)][string]$Channel ) if ($ManifestUrl -match '\.json($|\?)') { return $ManifestUrl } return ($ManifestUrl.TrimEnd('/') + "/$Channel/version.json") } if ([string]::IsNullOrWhiteSpace($ManifestUrl)) { throw "ManifestUrl is required." } $resolvedManifestUrl = Resolve-ManifestUrl -ManifestUrl $ManifestUrl -Channel $Channel Write-Host "Checking update manifest: $resolvedManifestUrl" $manifest = Get-OCSentinelManifest -Uri $resolvedManifestUrl if (-not $manifest.version -or -not $manifest.artifactUrl -or -not $manifest.sha256) { throw "Update manifest is missing required fields: version, artifactUrl, sha256." } $installedVersion = Get-InstalledVersion $availableVersion = [string]$manifest.version Write-Host "Installed version: $installedVersion" Write-Host "Available version: $availableVersion" if (-not $Force -and (Compare-Version -Left $installedVersion -Right $availableVersion) -ge 0) { Write-Host "OfficeCom Sentinel is already up to date." exit 0 } $downloadRoot = Join-Path $TempRoot ([Guid]::NewGuid().ToString("N")) $zipPath = Join-Path $downloadRoot "OCSentinelClient.zip" $extractRoot = Join-Path $downloadRoot "payload" New-Item -ItemType Directory -Force -Path $downloadRoot, $extractRoot | Out-Null Write-Host "Downloading artifact: $($manifest.artifactUrl)" Invoke-WebRequest -Uri ([string]$manifest.artifactUrl) -OutFile $zipPath -TimeoutSec 300 $actualHash = Get-Sha256Hex -Path $zipPath $expectedHash = ([string]$manifest.sha256).ToLowerInvariant() if ($actualHash -ne $expectedHash) { throw "SHA-256 mismatch for downloaded artifact. Expected $expectedHash but got $actualHash." } Write-Host "Artifact hash verified" Expand-Archive -Path $zipPath -DestinationPath $extractRoot -Force $payloadAppExe = Get-ChildItem -Path $extractRoot -Recurse -Filter "OCSentinelCli.exe" | Select-Object -First 1 if ($null -eq $payloadAppExe) { throw "Downloaded payload did not contain OCSentinelCli.exe" } $signature = Get-AuthenticodeSignature -FilePath $payloadAppExe.FullName if ($signature.Status -notin @("Valid", "NotSigned")) { throw "Executable signature validation failed with status: $($signature.Status)" } if ($signature.Status -eq "NotSigned") { Write-Warning "Downloaded executable is not code-signed yet. Hash validation succeeded, but code signing should be added before production rollout." } else { Write-Host "Executable signature verified: $($signature.SignerCertificate.Subject)" } $payloadInstallScript = Get-ChildItem -Path $extractRoot -Recurse -Filter "install-ocsentinel.ps1" | Select-Object -First 1 if ($null -eq $payloadInstallScript) { throw "Downloaded payload did not contain install-ocsentinel.ps1" } Write-Host "Installing OfficeCom Sentinel $availableVersion" & powershell.exe -ExecutionPolicy Bypass -File $payloadInstallScript.FullName if ($LASTEXITCODE -ne 0) { throw "Installer exited with code $LASTEXITCODE" } Write-Host "Update complete: $installedVersion -> $availableVersion"