diff --git a/docs/ocsentinel-deployment.md b/docs/ocsentinel-deployment.md index 5d3eef3..c231ee3 100644 --- a/docs/ocsentinel-deployment.md +++ b/docs/ocsentinel-deployment.md @@ -32,7 +32,16 @@ powershell -ExecutionPolicy Bypass -File .\build\build-release-manifest.ps1 ` ## NinjaOne Tasks -Initial install/update: +Create a PowerShell script in NinjaOne named `OCSentinel - Installieren oder aktualisieren`. +Run it as `SYSTEM` in 64-bit PowerShell and copy the content of +`scripts/bootstrap-ocsentinel-ninja.ps1` into the NinjaOne script editor. +It is idempotent: new devices install the current package, while installed devices +only update when a newer manifest version is published. + +Use it for the one-time rollout and, later, as the monthly update task. For an +initial validation scan, add `-RunInitialStatusScan` to the script parameters. + +Installed-client update only: ```powershell & "C:\Program Files\OCSentinel\scripts\update-ocsentinel.ps1" ` diff --git a/scripts/bootstrap-ocsentinel-ninja.ps1 b/scripts/bootstrap-ocsentinel-ninja.ps1 new file mode 100644 index 0000000..7bb5202 --- /dev/null +++ b/scripts/bootstrap-ocsentinel-ninja.ps1 @@ -0,0 +1,95 @@ +[CmdletBinding()] +param( + [string]$ManifestUrl = "https://gitea.officecom.cloud/officecom/oc-sentinel/raw/main/release/stable/version.json", + [switch]$RunInitialStatusScan +) + +$ErrorActionPreference = "Stop" +$ProgressPreference = "SilentlyContinue" +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +$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" + +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." + & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $updaterPath -ManifestUrl $ManifestUrl + if ($LASTEXITCODE -ne 0) { + throw "OCSentinel updater exited with code $LASTEXITCODE" + } +} +else { + Write-Host "Reading OCSentinel release manifest: $ManifestUrl" + $manifest = Invoke-RestMethod -Method Get -Uri $ManifestUrl -TimeoutSec 60 + 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)" + Invoke-WebRequest -Uri ([string]$manifest.artifactUrl) -OutFile $zipPath -TimeoutSec 300 + + $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 ($RunInitialStatusScan) { + if (-not (Test-Path -LiteralPath $monitorPath)) { + throw "OCSentinel was installed, but the monitor script is missing." + } + + Write-Host "Running initial OCSentinel status scan." + & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $monitorPath -Mode status -OutputPath "..\\reports\\ocsentinel-summary.json" + if ($LASTEXITCODE -ne 0) { + throw "Initial OCSentinel status scan exited with code $LASTEXITCODE" + } +} + +Write-Host "OCSentinel bootstrap completed successfully."