[CmdletBinding()] param( [string]$ManifestUrl = "", [ValidateSet("stable", "beta")] [string]$ReleaseChannel = "stable", [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) } } } function Enable-OCSentinelBetaDefaults { param([Parameter(Mandatory)][string]$SettingsPath) if (-not (Test-Path -LiteralPath $SettingsPath)) { return } $settings = Get-Content -LiteralPath $SettingsPath -Raw | ConvertFrom-Json if ($null -ne $settings.PSObject.Properties["ransomwareBetaDefaultApplied"]) { return } if ($null -eq $settings.PSObject.Properties["ransomwareBetaEnabled"]) { $settings | Add-Member -NotePropertyName "ransomwareBetaEnabled" -NotePropertyValue $true } else { $settings.ransomwareBetaEnabled = $true } $settings | Add-Member -NotePropertyName "ransomwareBetaDefaultApplied" -NotePropertyValue "1.5.0-beta.4" $settings | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $SettingsPath -Encoding UTF8 Write-Host "Enabled passive ransomware beta defaults." } Initialize-OCSentinelTls if ($ReleaseChannel -eq "stable" -and -not [string]::IsNullOrWhiteSpace($env:ReleaseChannel)) { $requestedChannel = $env:ReleaseChannel.Trim().ToLowerInvariant() if ($requestedChannel -notin @("stable", "beta")) { throw "ReleaseChannel must be stable or beta." } $ReleaseChannel = $requestedChannel } if ([string]::IsNullOrWhiteSpace($ManifestUrl)) { $ManifestUrl = "https://gitea.officecom.cloud/officecom/oc-sentinel/raw/main/release/$ReleaseChannel/version.json" } Write-Host "OCSentinel release channel: $ReleaseChannel" $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" $settingsPath = Join-Path $installRoot "config\ocsentinel-settings.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 ($ReleaseChannel -eq "beta") { Enable-OCSentinelBetaDefaults -SettingsPath $settingsPath } 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."