Files
oc-sentinel/scripts/bootstrap-ocsentinel-ninja.ps1
OfficeCom Codex 2cf3281b4d
Some checks failed
OfficeCom Sentinel Client / validate-client (push) Failing after 24s
OfficeCom Sentinel Client / build-client-windows (push) Has been cancelled
Retry interrupted client package downloads
2026-07-27 11:13:48 +02:00

256 lines
10 KiB
PowerShell

[CmdletBinding()]
param(
[string]$ManifestUrl = "https://gitea.officecom.cloud/officecom/oc-sentinel/raw/main/release/stable/version.json",
[string]$WebhookUrl = "",
[string]$SecretValue = "",
[switch]$RunInitialStatusScan
)
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
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)
}
}
}
function Invoke-OCSentinelUpdater {
param(
[Parameter(Mandatory)][string]$UpdaterPath,
[Parameter(Mandatory)][string]$ManifestUri
)
# Existing clients can still contain an older updater without TLS setup.
# Start it in a prepared child process so it can download the current package.
$escapedUpdaterPath = $UpdaterPath.Replace("'", "''")
$escapedManifestUri = $ManifestUri.Replace("'", "''")
$command = @"
`$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
& '$escapedUpdaterPath' -ManifestUrl '$escapedManifestUri'
exit `$LASTEXITCODE
"@
for ($attempt = 1; $attempt -le 3; $attempt++) {
& powershell.exe -NoProfile -ExecutionPolicy Bypass -Command $command | ForEach-Object { Write-Host $_ }
$exitCode = $LASTEXITCODE
if ($exitCode -eq 0) {
return
}
if ($attempt -lt 3) {
Write-Warning "OCSentinel update attempt $attempt failed. Retrying."
Start-Sleep -Seconds (5 * $attempt)
}
}
throw "OCSentinel updater exited with code $exitCode after 3 attempts."
}
function Get-OCSentinelArtifact {
param(
[Parameter(Mandatory)][string]$Uri,
[Parameter(Mandatory)][string]$DestinationPath
)
$parameters = @{ Uri = $Uri; OutFile = $DestinationPath; TimeoutSec = 300 }
if ((Get-Command Invoke-WebRequest).Parameters.ContainsKey("UseBasicParsing")) {
$parameters.UseBasicParsing = $true
}
for ($attempt = 1; $attempt -le 3; $attempt++) {
try {
Remove-Item -LiteralPath $DestinationPath -Force -ErrorAction SilentlyContinue
Invoke-WebRequest @parameters
if (-not (Test-Path -LiteralPath $DestinationPath) -or (Get-Item -LiteralPath $DestinationPath).Length -eq 0) {
throw "The downloaded artifact is empty."
}
return
}
catch {
if ($attempt -eq 3) {
throw "Could not download the OCSentinel package after 3 attempts. Last error: $($_.Exception.Message)"
}
Write-Warning "Package download attempt $attempt failed. Retrying."
Start-Sleep -Seconds (5 * $attempt)
}
}
}
Initialize-OCSentinelTls
$installRoot = Join-Path $env:ProgramFiles "OCSentinel"
$updaterPath = Join-Path $installRoot "scripts\update-ocsentinel.ps1"
$monitorPath = Join-Path $installRoot "scripts\run-ocsentinel-monitor.ps1"
$appPath = Join-Path $installRoot "app\OCSentinelCli.exe"
$clientConfigPath = Join-Path $installRoot "config\ocsentinel-client.json"
$secretScriptPath = Join-Path $installRoot "scripts\protect-ocsentinel-secret.ps1"
$secretPath = "C:\ProgramData\OCSentinel\secrets\ocsentinel-upload-secret.dat"
# NinjaOne script variables are exposed as process environment variables.
if ([string]::IsNullOrWhiteSpace($WebhookUrl)) {
$WebhookUrl = $env:WebhookUrl
}
if ([string]::IsNullOrWhiteSpace($SecretValue)) {
$SecretValue = $env:SecretValue
}
$runInitialScan = $RunInitialStatusScan.IsPresent
if (-not $runInitialScan -and -not [string]::IsNullOrWhiteSpace($env:RunInitialStatusScan)) {
$runInitialScan = $env:RunInitialStatusScan -match '^(1|true|yes|on)$'
}
function Assert-ArtifactSignature {
param([Parameter(Mandatory)][string]$ExecutablePath)
$signature = Get-AuthenticodeSignature -FilePath $ExecutablePath
if ($signature.Status -notin @("Valid", "NotSigned")) {
throw "Executable signature validation failed with status: $($signature.Status)"
}
if ($signature.Status -eq "NotSigned") {
Write-Warning "The package hash was verified, but OCSentinelCli.exe is not code-signed yet."
}
}
if (Test-Path -LiteralPath $updaterPath) {
Write-Host "Existing OCSentinel installation found. Checking for updates."
Invoke-OCSentinelUpdater -UpdaterPath $updaterPath -ManifestUri $ManifestUrl
}
else {
Write-Host "Reading OCSentinel release manifest: $ManifestUrl"
$manifest = Get-OCSentinelManifest -Uri $ManifestUrl
if ([string]::IsNullOrWhiteSpace($manifest.version) -or [string]::IsNullOrWhiteSpace($manifest.artifactUrl) -or [string]::IsNullOrWhiteSpace($manifest.sha256)) {
throw "Release manifest is missing version, artifactUrl, or sha256."
}
$downloadRoot = Join-Path $env:ProgramData ("OCSentinel\\bootstrap\\" + [Guid]::NewGuid().ToString("N"))
$zipPath = Join-Path $downloadRoot "OCSentinelClient.zip"
$extractRoot = Join-Path $downloadRoot "payload"
try {
New-Item -ItemType Directory -Force -Path $extractRoot | Out-Null
Write-Host "Downloading OCSentinel $($manifest.version)"
Get-OCSentinelArtifact -Uri ([string]$manifest.artifactUrl) -DestinationPath $zipPath
$actualHash = (Get-FileHash -LiteralPath $zipPath -Algorithm SHA256).Hash.ToLowerInvariant()
$expectedHash = ([string]$manifest.sha256).ToLowerInvariant()
if ($actualHash -ne $expectedHash) {
throw "SHA-256 mismatch for the downloaded OCSentinel package."
}
Write-Host "Package hash verified. Extracting release payload."
Expand-Archive -LiteralPath $zipPath -DestinationPath $extractRoot -Force
$payloadApp = Get-ChildItem -Path $extractRoot -Recurse -Filter "OCSentinelCli.exe" | Select-Object -First 1
$installer = Get-ChildItem -Path $extractRoot -Recurse -Filter "install-ocsentinel.ps1" | Select-Object -First 1
if ($null -eq $payloadApp -or $null -eq $installer) {
throw "The downloaded package is incomplete."
}
Assert-ArtifactSignature -ExecutablePath $payloadApp.FullName
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $installer.FullName
if ($LASTEXITCODE -ne 0) {
throw "OCSentinel installer exited with code $LASTEXITCODE"
}
}
finally {
if (Test-Path -LiteralPath $downloadRoot) {
Remove-Item -LiteralPath $downloadRoot -Recurse -Force
}
}
}
if (-not (Test-Path -LiteralPath $appPath)) {
throw "OCSentinel installation completed, but the client executable was not found."
}
if (-not [string]::IsNullOrWhiteSpace($WebhookUrl)) {
if (-not (Test-Path -LiteralPath $clientConfigPath)) {
throw "OCSentinel client configuration was not found: $clientConfigPath"
}
$clientConfig = Get-Content -LiteralPath $clientConfigPath -Raw | ConvertFrom-Json
$clientConfig.n8nWebhookUrl = $WebhookUrl
$clientConfig.environment = "production"
$ninjaContext = @(
@{ EnvironmentName = "NINJA_ORGANIZATION_ID"; PropertyName = "ninjaOrganizationId" },
@{ EnvironmentName = "NINJA_ORGANIZATION_NAME"; PropertyName = "ninjaOrganizationName" },
@{ EnvironmentName = "NINJA_AGENT_MACHINE_ID"; PropertyName = "ninjaMachineId" },
@{ EnvironmentName = "NINJA_AGENT_NODE_ID"; PropertyName = "ninjaNodeId" },
@{ EnvironmentName = "NINJA_LOCATION_ID"; PropertyName = "ninjaLocationId" },
@{ EnvironmentName = "NINJA_LOCATION_NAME"; PropertyName = "ninjaLocationName" }
)
foreach ($entry in $ninjaContext) {
$value = [Environment]::GetEnvironmentVariable($entry.EnvironmentName, "Process")
if (-not [string]::IsNullOrWhiteSpace($value)) {
$clientConfig | Add-Member -NotePropertyName $entry.PropertyName -NotePropertyValue $value.Trim() -Force
}
}
$clientConfig | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $clientConfigPath -Encoding UTF8
Write-Host "Configured OCSentinel upload endpoint and NinjaOne context."
}
if (-not [string]::IsNullOrWhiteSpace($SecretValue)) {
if (-not (Test-Path -LiteralPath $secretScriptPath)) {
throw "OCSentinel secret bootstrap script was not found: $secretScriptPath"
}
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $secretScriptPath -SecretValue $SecretValue
if ($LASTEXITCODE -ne 0) {
throw "OCSentinel secret bootstrap failed with code $LASTEXITCODE"
}
}
if ($runInitialScan) {
if (-not (Test-Path -LiteralPath $monitorPath)) {
throw "OCSentinel was installed, but the monitor script is missing."
}
Write-Host "Running initial OCSentinel status scan."
$scanArguments = @("-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $monitorPath, "-Mode", "status", "-OutputPath", "..\\reports\\ocsentinel-summary.json")
if ((Test-Path -LiteralPath $clientConfigPath) -and (Test-Path -LiteralPath $secretPath)) {
$scanArguments += @("-ClientConfigPath", $clientConfigPath, "-SecretPath", $secretPath, "-UploadMode", "required")
}
& powershell.exe @scanArguments
if ($LASTEXITCODE -ne 0) {
throw "Initial OCSentinel status scan exited with code $LASTEXITCODE"
}
}
Write-Host "OCSentinel bootstrap completed successfully."