9 Commits

Author SHA1 Message Date
OfficeCom Codex
1c2170090e Stagger daily scans across early morning
Some checks failed
OfficeCom Sentinel Client / validate-client (push) Successful in 23s
OfficeCom Sentinel Client / build-client-windows (push) Failing after 18s
2026-07-26 02:24:05 +02:00
OfficeCom Codex
91c5794502 Add daily scans and Ninja burst mode
Some checks failed
OfficeCom Sentinel Client / validate-client (push) Successful in 23s
OfficeCom Sentinel Client / build-client-windows (push) Failing after 18s
2026-07-26 02:17:03 +02:00
OfficeCom Codex
f392057535 Document n8n raw body signature validation
Some checks failed
OfficeCom Sentinel Client / validate-client (push) Successful in 24s
OfficeCom Sentinel Client / build-client-windows (push) Failing after 18s
2026-07-25 21:26:56 +02:00
OfficeCom Codex
7431c656d3 Use documented NinjaOne custom field commands
Some checks failed
OfficeCom Sentinel Client / validate-client (push) Successful in 23s
OfficeCom Sentinel Client / build-client-windows (push) Failing after 18s
2026-07-25 21:17:59 +02:00
OfficeCom Codex
49b0e3025d Fix Ninja client configuration parsing
Some checks failed
OfficeCom Sentinel Client / validate-client (push) Successful in 24s
OfficeCom Sentinel Client / build-client-windows (push) Failing after 18s
2026-07-25 21:14:50 +02:00
OfficeCom Codex
aefec51581 Fix NinjaOne environment variable lookup
Some checks failed
OfficeCom Sentinel Client / validate-client (push) Successful in 24s
OfficeCom Sentinel Client / build-client-windows (push) Failing after 19s
2026-07-25 21:10:24 +02:00
OfficeCom Codex
7555e92aac Add one-time NinjaOne installation script
Some checks failed
OfficeCom Sentinel Client / validate-client (push) Successful in 25s
OfficeCom Sentinel Client / build-client-windows (push) Failing after 19s
2026-07-25 21:01:02 +02:00
OfficeCom Codex
b97b554f84 Add NinjaOne client configuration and upload test
Some checks failed
OfficeCom Sentinel Client / validate-client (push) Successful in 25s
OfficeCom Sentinel Client / build-client-windows (push) Failing after 19s
2026-07-25 20:58:54 +02:00
OfficeCom Codex
77d7eaa0b5 Read NinjaOne rollout variables from environment
Some checks failed
OfficeCom Sentinel Client / validate-client (push) Successful in 24s
OfficeCom Sentinel Client / build-client-windows (push) Failing after 19s
2026-07-25 18:53:34 +02:00
15 changed files with 384 additions and 52 deletions

View File

@@ -54,6 +54,7 @@ Copy-Item -Path (Join-Path $installerRoot "uninstall-ocsentinel.ps1") -Destinati
Copy-Item -Path (Join-Path $installerRoot "update-ocsentinel.ps1") -Destination (Join-Path $packageRoot "scripts\update-ocsentinel.ps1") -Force
Copy-Item -Path (Join-Path $installerRoot "runtime-run-ocsentinel.ps1") -Destination (Join-Path $packageRoot "scripts\run-ocsentinel.ps1") -Force
Copy-Item -Path (Join-Path $installerRoot "runtime-run-ocsentinel-monitor.ps1") -Destination (Join-Path $packageRoot "scripts\run-ocsentinel-monitor.ps1") -Force
Copy-Item -Path (Join-Path $installerRoot "runtime-run-ocsentinel-scheduled.ps1") -Destination (Join-Path $packageRoot "scripts\run-ocsentinel-scheduled.ps1") -Force
Copy-Item -Path (Join-Path $repoRoot "scripts\protect-ocsentinel-secret.ps1") -Destination (Join-Path $packageRoot "scripts\protect-ocsentinel-secret.ps1") -Force
Copy-Item -Path (Join-Path $repoRoot "config\ocsentinel-settings.example.json") -Destination (Join-Path $packageRoot "config\ocsentinel-settings.example.json") -Force

View File

@@ -30,6 +30,22 @@ powershell -ExecutionPolicy Bypass -File .\build\build-release-manifest.ps1 `
- `config\ocsentinel-settings.json`
- `config\ocsentinel-client.json`
## Local Schedule And Burst Mode
The installer creates two Windows Scheduled Tasks running as `SYSTEM`:
- `OCSentinel Daily Scan`: runs once per day and uploads one signed report.
The installer deterministically assigns each device a stable slot between
`04:00` and `06:59`, derived from its Windows `MachineGuid`. This distributes
a fleet rollout instead of sending all reports at the same time.
- `OCSentinel Burst Check`: runs every five minutes. It performs no scan unless
the NinjaOne device custom field `ocsentinelburst` is enabled.
Create `ocsentinelburst` as a device-level `Checkbox` custom field and allow
automation read access. Set it to `true` for a device to begin the five-minute
burst scans; clear it to stop them. The normal daily scan continues regardless
of the checkbox.
## NinjaOne Tasks
Create a PowerShell script in NinjaOne named `OCSentinel - Installieren oder aktualisieren`.

View File

@@ -36,7 +36,7 @@ For the isolated development environment only, HTTP is permitted at
`http://172.16.41.197:5678/webhook/ocsentinel-ingest`. Do not reuse this URL,
the development shared secret, or a disabled-TLS configuration in production.
1. `Webhook`: accept `POST` on the configured private URL.
1. `Webhook`: accept `POST` on the configured private URL and enable **Raw Body**.
2. `Code`: reject a request if `X-ATN-Device`, `X-ATN-Timestamp`,
`X-ATN-Nonce`, `X-ATN-Version`, `X-ATN-Payload-SHA256`, or
`X-ATN-Signature` is missing; reject timestamps outside five minutes.
@@ -49,6 +49,11 @@ the development shared secret, or a disabled-TLS configuration in production.
<device>\n<timestamp>\n<nonce>\n<version>\n<payload-sha256>
```
In the current n8n Webhook node, the raw bytes are exposed as Base64 at
`$binary.data.data`. Decode this value before calculating the payload hash.
Do not hash `JSON.stringify($json.body)`: parsing and reserializing JSON
changes whitespace and can change the signed byte sequence.
4. `Postgres`: insert the nonce into `ocsentinel.ingest_nonce` with a short
expiry. If it already exists, return `409` and do not process the report.
5. `Postgres`: upsert the device, insert a row in `ocsentinel.scan_report`,

View File

@@ -32,6 +32,7 @@ if (Test-Path (Join-Path $packageRoot "config\ocsentinel-client.dev.example.json
Copy-Item -Path (Join-Path $packageRoot "samples\ninja-vulnerability-export.example.csv") -Destination (Join-Path $samplesRoot "ninja-vulnerability-export.example.csv") -Force
Copy-Item -Path (Join-Path $packageRoot "scripts\run-ocsentinel.ps1") -Destination $scriptRoot -Force
Copy-Item -Path (Join-Path $packageRoot "scripts\run-ocsentinel-monitor.ps1") -Destination $scriptRoot -Force
Copy-Item -Path (Join-Path $packageRoot "scripts\run-ocsentinel-scheduled.ps1") -Destination $scriptRoot -Force
if (Test-Path (Join-Path $packageRoot "scripts\protect-ocsentinel-secret.ps1")) {
Copy-Item -Path (Join-Path $packageRoot "scripts\protect-ocsentinel-secret.ps1") -Destination $scriptRoot -Force
}
@@ -69,8 +70,34 @@ Set-ItemProperty -Path $uninstallKey -Name "QuietUninstallString" -Value $uninst
Set-ItemProperty -Path $uninstallKey -Name "NoModify" -Value 1 -Type DWord
Set-ItemProperty -Path $uninstallKey -Name "NoRepair" -Value 1 -Type DWord
$scheduledScript = Join-Path $scriptRoot "run-ocsentinel-scheduled.ps1"
$taskPrincipal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
$taskSettings = New-ScheduledTaskSettingsSet -StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Minutes 30) -MultipleInstances IgnoreNew
# Spread fleet uploads across the early-morning window while keeping each device's slot stable.
$machineGuid = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Cryptography" -Name "MachineGuid").MachineGuid
$guidBytes = [Text.Encoding]::UTF8.GetBytes([string]$machineGuid)
$sha256 = [Security.Cryptography.SHA256]::Create()
try {
$slotHash = $sha256.ComputeHash($guidBytes)
}
finally {
$sha256.Dispose()
}
$dailySlotMinutes = [BitConverter]::ToUInt32($slotHash, 0) % 180
$dailyRunAt = (Get-Date -Hour 4 -Minute 0 -Second 0).AddMinutes($dailySlotMinutes)
$dailyAction = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$scheduledScript`" -Kind daily" -WorkingDirectory $scriptRoot
$dailyTrigger = New-ScheduledTaskTrigger -Daily -At $dailyRunAt
Register-ScheduledTask -TaskName "OCSentinel Daily Scan" -Action $dailyAction -Trigger $dailyTrigger -Principal $taskPrincipal -Settings $taskSettings -Description "OfficeCom Sentinel daily signed scan and upload." -Force | Out-Null
$burstAction = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$scheduledScript`" -Kind burst" -WorkingDirectory $scriptRoot
$burstTrigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(2) -RepetitionInterval (New-TimeSpan -Minutes 5) -RepetitionDuration (New-TimeSpan -Days 3650)
Register-ScheduledTask -TaskName "OCSentinel Burst Check" -Action $burstAction -Trigger $burstTrigger -Principal $taskPrincipal -Settings $taskSettings -Description "OfficeCom Sentinel burst check; scans only when Ninja field ocsentinelburst is enabled." -Force | Out-Null
Write-Host "Installation complete."
Write-Host "Main path: $installRoot"
Write-Host "Runner: $(Join-Path $scriptRoot 'run-ocsentinel.ps1')"
Write-Host "Monitor: $(Join-Path $scriptRoot 'run-ocsentinel-monitor.ps1')"
Write-Host "Updater: $(Join-Path $scriptRoot 'update-ocsentinel.ps1')"
Write-Host "Schedule: Daily scan at $($dailyRunAt.ToString('HH:mm')) (deterministic 04:00-06:59 slot); burst check every 5 minutes."

View File

@@ -9,6 +9,7 @@ param(
[string]$UploadMode = "auto",
[string]$VulnerabilityCsvPath = "",
[string]$MirrorRoot = "",
[switch]$SuppressTriggerExit,
[ValidateSet("status", "attack-only", "cve-critical", "attack-plus-cve")]
[string]$Mode = "status"
)
@@ -46,8 +47,13 @@ function Initialize-NinjaFieldWriter {
return
}
if (Get-Command -Name "Set-NinjaProperty" -ErrorAction SilentlyContinue) {
$script:NinjaFieldBackend = "powershell-modern"
return
}
if (Get-Command -Name "Ninja-Property-Set" -ErrorAction SilentlyContinue) {
$script:NinjaFieldBackend = "powershell"
$script:NinjaFieldBackend = "powershell-legacy"
return
}
@@ -64,14 +70,20 @@ function Set-NinjaCustomFieldValue {
[Parameter(Mandatory)]
[string]$Name,
[AllowEmptyString()]
[string]$Value
[object]$Value,
[Parameter(Mandatory)]
[string]$Type
)
Initialize-NinjaFieldWriter
switch ($script:NinjaFieldBackend) {
"powershell" {
Ninja-Property-Set $Name $Value | Out-Null
"powershell-modern" {
Set-NinjaProperty -Name $Name -Value $Value -Type $Type -Force | Out-Null
return $true
}
"powershell-legacy" {
Ninja-Property-Set -Name $Name -Value $Value | Out-Null
return $true
}
"cli" {
@@ -112,28 +124,28 @@ function Publish-NinjaCustomFields {
}
}
$fieldValues = [ordered]@{
"ocsentinelstatus" = [string]$Report.AlertState
"ocsentinelreason" = $Reason
"ocsentinelbasestatus" = [string]$Report.BaseAlertState
"ocsentinelevents" = [string]([int]$Report.TotalEvents)
"ocsentineluniqueips" = [string]([int]$Report.UniqueIpCount)
"ocsentinelcvecritical" = [string]([int]$Report.VulnerabilityCorrelation.CriticalCount)
"ocsentinelcvetotal" = [string]([int]$Report.VulnerabilityCorrelation.TotalCount)
"ocsentinelmode" = $Mode
"ocsentineltriggered" = $Triggered.ToString().ToLowerInvariant()
"ocsentinellastscanutc" = $generatedAtUtc
}
$fieldValues = @(
[pscustomobject]@{ Name = "ocsentinelstatus"; Type = "Text"; Value = [string]$Report.AlertState }
[pscustomobject]@{ Name = "ocsentinelreason"; Type = "Text"; Value = $Reason }
[pscustomobject]@{ Name = "ocsentinelbasestatus"; Type = "Text"; Value = [string]$Report.BaseAlertState }
[pscustomobject]@{ Name = "ocsentinelevents"; Type = "Integer"; Value = [int]$Report.TotalEvents }
[pscustomobject]@{ Name = "ocsentineluniqueips"; Type = "Integer"; Value = [int]$Report.UniqueIpCount }
[pscustomobject]@{ Name = "ocsentinelcvecritical"; Type = "Integer"; Value = [int]$Report.VulnerabilityCorrelation.CriticalCount }
[pscustomobject]@{ Name = "ocsentinelcvetotal"; Type = "Integer"; Value = [int]$Report.VulnerabilityCorrelation.TotalCount }
[pscustomobject]@{ Name = "ocsentinelmode"; Type = "Text"; Value = $Mode }
[pscustomobject]@{ Name = "ocsentineltriggered"; Type = "Checkbox"; Value = $Triggered }
[pscustomobject]@{ Name = "ocsentinellastscanutc"; Type = "DateTime"; Value = $generatedAtUtc }
)
$updated = 0
foreach ($entry in $fieldValues.GetEnumerator()) {
foreach ($entry in $fieldValues) {
try {
if (Set-NinjaCustomFieldValue -Name $entry.Key -Value $entry.Value) {
if (Set-NinjaCustomFieldValue -Name $entry.Name -Value $entry.Value -Type $entry.Type) {
$updated++
}
}
catch {
Write-Warning "Failed to set Ninja custom field '$($entry.Key)': $($_.Exception.Message)"
Write-Warning "Failed to set Ninja custom field '$($entry.Name)': $($_.Exception.Message)"
}
}
@@ -216,8 +228,8 @@ Write-Host "Total CVEs: $totalCves"
Write-Host "Report: $outputFullPath"
Write-Host "Runner exit code: $runnerExitCode"
if ($monitorTriggered) {
if ($monitorTriggered -and -not $SuppressTriggerExit) {
exit 1
}
exit 0
exit $runnerExitCode

View File

@@ -0,0 +1,50 @@
[CmdletBinding()]
param(
[ValidateSet("daily", "burst")]
[string]$Kind = "daily"
)
$ErrorActionPreference = "Stop"
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$monitorScript = Join-Path $scriptDir "run-ocsentinel-monitor.ps1"
$secretPath = "C:\ProgramData\OCSentinel\secrets\ocsentinel-upload-secret.dat"
$mutexName = "Global\OfficeComSentinelScan"
function Get-NinjaBurstEnabled {
if (Get-Command -Name "Get-NinjaProperty" -ErrorAction SilentlyContinue) {
return [bool](Get-NinjaProperty -Name "ocsentinelburst" -Type "Checkbox")
}
if (Get-Command -Name "Ninja-Property-Get" -ErrorAction SilentlyContinue) {
$value = Ninja-Property-Get -Name "ocsentinelburst"
return [string]$value -match "^(1|true|yes)$"
}
Write-Warning "Ninja custom-field reader is unavailable; burst scan skipped."
return $false
}
if ($Kind -eq "burst" -and -not (Get-NinjaBurstEnabled)) {
Write-Host "OfficeCom Sentinel burst check: disabled."
exit 0
}
$createdNew = $false
$mutex = [Threading.Mutex]::new($false, $mutexName, [ref]$createdNew)
try {
if (-not $mutex.WaitOne(0)) {
Write-Host "OfficeCom Sentinel scan skipped: another scan is already running."
exit 0
}
Write-Host "OfficeCom Sentinel scheduled $Kind scan started."
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $monitorScript -Mode status -UploadMode required -SecretPath $secretPath -SuppressTriggerExit
exit $LASTEXITCODE
}
finally {
if ($null -ne $mutex) {
try { $mutex.ReleaseMutex() } catch { }
$mutex.Dispose()
}
}

View File

@@ -5,6 +5,12 @@ $ErrorActionPreference = "Stop"
$installRoot = Join-Path ${env:ProgramFiles} "OCSentinel"
$uninstallKey = "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\OCSentinel"
foreach ($taskName in @("OCSentinel Daily Scan", "OCSentinel Burst Check")) {
if (Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue) {
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false
}
}
if (Test-Path $uninstallKey) {
Remove-Item -Path $uninstallKey -Force -Recurse
}

View File

@@ -1,8 +1,8 @@
{
"channel": "stable",
"version": "1.2.6",
"publishedAtUtc": "2026-07-25T00:00:00Z",
"artifactUrl": "https://gitea.officecom.cloud/officecom/oc-sentinel/releases/download/v1.2.6/OCSentinelClient-win-x64.zip",
"sha256": "d6256f3e376701bff18d9dfd8d5825498e85fb6d94c15227828a50678693d097",
"version": "1.3.2",
"publishedAtUtc": "2026-07-26T00:23:31.3024295Z",
"artifactUrl": "https://gitea.officecom.cloud/officecom/oc-sentinel/releases/download/v1.3.2/OCSentinelClient-win-x64.zip",
"sha256": "dbfedf0890176873cef8bc91b9f7a6d6c3c8382ffadc972324b9900bad1153cc",
"minUpdaterVersion": "1.0.0"
}

View File

@@ -18,6 +18,20 @@ $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)
@@ -108,7 +122,7 @@ if (-not [string]::IsNullOrWhiteSpace($SecretValue)) {
}
}
if ($RunInitialStatusScan) {
if ($runInitialScan) {
if (-not (Test-Path -LiteralPath $monitorPath)) {
throw "OCSentinel was installed, but the monitor script is missing."
}

View File

@@ -0,0 +1,62 @@
[CmdletBinding()]
param(
[string]$WebhookUrl = "",
[string]$SecretValue = ""
)
$ErrorActionPreference = "Stop"
function Get-NinjaValue {
param([Parameter(Mandatory)][string]$Name)
$value = [Environment]::GetEnvironmentVariable($Name, "Process")
if ($null -eq $value) {
return ""
}
return $value.Trim()
}
if ([string]::IsNullOrWhiteSpace($WebhookUrl)) {
$WebhookUrl = Get-NinjaValue -Name "webhookurl"
}
if ([string]::IsNullOrWhiteSpace($SecretValue)) {
$SecretValue = Get-NinjaValue -Name "secretvalue"
}
if ([string]::IsNullOrWhiteSpace($WebhookUrl) -or [string]::IsNullOrWhiteSpace($SecretValue)) {
throw "WebhookUrl and SecretValue must be supplied as NinjaOne script variables."
}
$installRoot = Join-Path $env:ProgramFiles "OCSentinel"
$configPath = Join-Path $installRoot "config\ocsentinel-client.json"
$secretScript = Join-Path $installRoot "scripts\protect-ocsentinel-secret.ps1"
$monitorScript = Join-Path $installRoot "scripts\run-ocsentinel-monitor.ps1"
$secretPath = "C:\ProgramData\OCSentinel\secrets\ocsentinel-upload-secret.dat"
foreach ($path in @($configPath, $secretScript, $monitorScript)) {
if (-not (Test-Path -LiteralPath $path)) {
throw "OCSentinel installation is incomplete. Missing: $path"
}
}
$config = Get-Content -LiteralPath $configPath -Raw | ConvertFrom-Json
$config.n8nWebhookUrl = $WebhookUrl
$config.environment = "production"
$config | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $configPath -Encoding UTF8
Write-Host "OCSentinel upload endpoint configured."
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $secretScript -SecretValue $SecretValue
if ($LASTEXITCODE -ne 0) {
throw "Writing the protected upload secret failed with code $LASTEXITCODE"
}
Write-Host "Running signed OCSentinel test scan and upload."
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $monitorScript `
-Mode status `
-ClientConfigPath $configPath `
-SecretPath $secretPath `
-UploadMode required
exit $LASTEXITCODE

View File

@@ -0,0 +1,74 @@
[CmdletBinding()]
param(
[string]$ManifestUrl = "https://gitea.officecom.cloud/officecom/oc-sentinel/raw/main/release/stable/version.json",
[string]$WebhookUrl = "",
[string]$SecretValue = ""
)
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
function Get-NinjaValue {
param([Parameter(Mandatory)][string]$Name)
$value = [Environment]::GetEnvironmentVariable($Name, "Process")
if ($null -eq $value) {
return ""
}
return $value.Trim()
}
if ([string]::IsNullOrWhiteSpace($WebhookUrl)) { $WebhookUrl = Get-NinjaValue -Name "webhookurl" }
if ([string]::IsNullOrWhiteSpace($SecretValue)) { $SecretValue = Get-NinjaValue -Name "secretvalue" }
if ([string]::IsNullOrWhiteSpace($WebhookUrl) -or [string]::IsNullOrWhiteSpace($SecretValue)) {
throw "WebhookUrl and SecretValue must be set as NinjaOne script variables."
}
$manifest = Invoke-RestMethod -Method Get -Uri $ManifestUrl -TimeoutSec 60
if ([string]::IsNullOrWhiteSpace($manifest.artifactUrl) -or [string]::IsNullOrWhiteSpace($manifest.sha256)) {
throw "The release manifest is incomplete."
}
$downloadRoot = Join-Path $env:ProgramData ("OCSentinel\\install-" + [Guid]::NewGuid().ToString("N"))
$zipPath = Join-Path $downloadRoot "OCSentinelClient.zip"
$extractPath = Join-Path $downloadRoot "payload"
try {
New-Item -ItemType Directory -Force -Path $extractPath | Out-Null
Write-Host "Downloading OCSentinel $($manifest.version)."
Invoke-WebRequest -Uri $manifest.artifactUrl -OutFile $zipPath -TimeoutSec 300
$actualHash = (Get-FileHash -LiteralPath $zipPath -Algorithm SHA256).Hash.ToLowerInvariant()
if ($actualHash -ne ([string]$manifest.sha256).ToLowerInvariant()) {
throw "Release package SHA-256 validation failed."
}
Expand-Archive -LiteralPath $zipPath -DestinationPath $extractPath -Force
$installer = Get-ChildItem -Path $extractPath -Recurse -Filter "install-ocsentinel.ps1" | Select-Object -First 1
if ($null -eq $installer) { throw "The release package does not contain the installer." }
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $installer.FullName
if ($LASTEXITCODE -ne 0) { throw "Installer failed with code $LASTEXITCODE" }
}
finally {
if (Test-Path -LiteralPath $downloadRoot) { Remove-Item -LiteralPath $downloadRoot -Recurse -Force }
}
$installRoot = Join-Path $env:ProgramFiles "OCSentinel"
$configPath = Join-Path $installRoot "config\ocsentinel-client.json"
$secretScript = Join-Path $installRoot "scripts\protect-ocsentinel-secret.ps1"
$monitorScript = Join-Path $installRoot "scripts\run-ocsentinel-monitor.ps1"
$secretPath = "C:\ProgramData\OCSentinel\secrets\ocsentinel-upload-secret.dat"
$config = Get-Content -LiteralPath $configPath -Raw | ConvertFrom-Json
$config.n8nWebhookUrl = $WebhookUrl
$config.environment = "production"
$config | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $configPath -Encoding UTF8
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $secretScript -SecretValue $SecretValue
if ($LASTEXITCODE -ne 0) { throw "Writing the protected upload secret failed with code $LASTEXITCODE" }
Write-Host "Running initial signed scan and upload."
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $monitorScript -Mode status -ClientConfigPath $configPath -SecretPath $secretPath -UploadMode required
exit $LASTEXITCODE

View File

@@ -9,6 +9,7 @@ param(
[string]$UploadMode = "auto",
[string]$VulnerabilityCsvPath = "",
[string]$MirrorRoot = "",
[switch]$SuppressTriggerExit,
[ValidateSet("status", "attack-only", "cve-critical", "attack-plus-cve")]
[string]$Mode = "status"
)
@@ -46,8 +47,13 @@ function Initialize-NinjaFieldWriter {
return
}
if (Get-Command -Name "Set-NinjaProperty" -ErrorAction SilentlyContinue) {
$script:NinjaFieldBackend = "powershell-modern"
return
}
if (Get-Command -Name "Ninja-Property-Set" -ErrorAction SilentlyContinue) {
$script:NinjaFieldBackend = "powershell"
$script:NinjaFieldBackend = "powershell-legacy"
return
}
@@ -64,14 +70,20 @@ function Set-NinjaCustomFieldValue {
[Parameter(Mandatory)]
[string]$Name,
[AllowEmptyString()]
[string]$Value
[object]$Value,
[Parameter(Mandatory)]
[string]$Type
)
Initialize-NinjaFieldWriter
switch ($script:NinjaFieldBackend) {
"powershell" {
Ninja-Property-Set $Name $Value | Out-Null
"powershell-modern" {
Set-NinjaProperty -Name $Name -Value $Value -Type $Type -Force | Out-Null
return $true
}
"powershell-legacy" {
Ninja-Property-Set -Name $Name -Value $Value | Out-Null
return $true
}
"cli" {
@@ -112,28 +124,28 @@ function Publish-NinjaCustomFields {
}
}
$fieldValues = [ordered]@{
"ocsentinelstatus" = [string]$Report.AlertState
"ocsentinelreason" = $Reason
"ocsentinelbasestatus" = [string]$Report.BaseAlertState
"ocsentinelevents" = [string]([int]$Report.TotalEvents)
"ocsentineluniqueips" = [string]([int]$Report.UniqueIpCount)
"ocsentinelcvecritical" = [string]([int]$Report.VulnerabilityCorrelation.CriticalCount)
"ocsentinelcvetotal" = [string]([int]$Report.VulnerabilityCorrelation.TotalCount)
"ocsentinelmode" = $Mode
"ocsentineltriggered" = $Triggered.ToString().ToLowerInvariant()
"ocsentinellastscanutc" = $generatedAtUtc
}
$fieldValues = @(
[pscustomobject]@{ Name = "ocsentinelstatus"; Type = "Text"; Value = [string]$Report.AlertState }
[pscustomobject]@{ Name = "ocsentinelreason"; Type = "Text"; Value = $Reason }
[pscustomobject]@{ Name = "ocsentinelbasestatus"; Type = "Text"; Value = [string]$Report.BaseAlertState }
[pscustomobject]@{ Name = "ocsentinelevents"; Type = "Integer"; Value = [int]$Report.TotalEvents }
[pscustomobject]@{ Name = "ocsentineluniqueips"; Type = "Integer"; Value = [int]$Report.UniqueIpCount }
[pscustomobject]@{ Name = "ocsentinelcvecritical"; Type = "Integer"; Value = [int]$Report.VulnerabilityCorrelation.CriticalCount }
[pscustomobject]@{ Name = "ocsentinelcvetotal"; Type = "Integer"; Value = [int]$Report.VulnerabilityCorrelation.TotalCount }
[pscustomobject]@{ Name = "ocsentinelmode"; Type = "Text"; Value = $Mode }
[pscustomobject]@{ Name = "ocsentineltriggered"; Type = "Checkbox"; Value = $Triggered }
[pscustomobject]@{ Name = "ocsentinellastscanutc"; Type = "DateTime"; Value = $generatedAtUtc }
)
$updated = 0
foreach ($entry in $fieldValues.GetEnumerator()) {
foreach ($entry in $fieldValues) {
try {
if (Set-NinjaCustomFieldValue -Name $entry.Key -Value $entry.Value) {
if (Set-NinjaCustomFieldValue -Name $entry.Name -Value $entry.Value -Type $entry.Type) {
$updated++
}
}
catch {
Write-Warning "Failed to set Ninja custom field '$($entry.Key)': $($_.Exception.Message)"
Write-Warning "Failed to set Ninja custom field '$($entry.Name)': $($_.Exception.Message)"
}
}
@@ -216,8 +228,8 @@ Write-Host "Total CVEs: $totalCves"
Write-Host "Report: $outputFullPath"
Write-Host "Runner exit code: $runnerExitCode"
if ($monitorTriggered) {
if ($monitorTriggered -and -not $SuppressTriggerExit) {
exit 1
}
exit 0
exit $runnerExitCode

View File

@@ -0,0 +1,50 @@
[CmdletBinding()]
param(
[ValidateSet("daily", "burst")]
[string]$Kind = "daily"
)
$ErrorActionPreference = "Stop"
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$monitorScript = Join-Path $scriptDir "run-ocsentinel-monitor.ps1"
$secretPath = "C:\ProgramData\OCSentinel\secrets\ocsentinel-upload-secret.dat"
$mutexName = "Global\OfficeComSentinelScan"
function Get-NinjaBurstEnabled {
if (Get-Command -Name "Get-NinjaProperty" -ErrorAction SilentlyContinue) {
return [bool](Get-NinjaProperty -Name "ocsentinelburst" -Type "Checkbox")
}
if (Get-Command -Name "Ninja-Property-Get" -ErrorAction SilentlyContinue) {
$value = Ninja-Property-Get -Name "ocsentinelburst"
return [string]$value -match "^(1|true|yes)$"
}
Write-Warning "Ninja custom-field reader is unavailable; burst scan skipped."
return $false
}
if ($Kind -eq "burst" -and -not (Get-NinjaBurstEnabled)) {
Write-Host "OfficeCom Sentinel burst check: disabled."
exit 0
}
$createdNew = $false
$mutex = [Threading.Mutex]::new($false, $mutexName, [ref]$createdNew)
try {
if (-not $mutex.WaitOne(0)) {
Write-Host "OfficeCom Sentinel scan skipped: another scan is already running."
exit 0
}
Write-Host "OfficeCom Sentinel scheduled $Kind scan started."
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $monitorScript -Mode status -UploadMode required -SecretPath $secretPath -SuppressTriggerExit
exit $LASTEXITCODE
}
finally {
if ($null -ne $mutex) {
try { $mutex.ReleaseMutex() } catch { }
$mutex.Dispose()
}
}

View File

@@ -8,6 +8,9 @@ internal static class JsonOptions
public static readonly JsonSerializerOptions Default = new()
{
WriteIndented = true,
// Client configuration is also written by PowerShell/NinjaOne scripts.
// Accept their conventional camelCase names (for example n8nWebhookUrl).
PropertyNameCaseInsensitive = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
}

View File

@@ -9,10 +9,10 @@
<RootNamespace>OCSentinelCli</RootNamespace>
<Product>OfficeCom Sentinel</Product>
<Company>OfficeCom</Company>
<Version>1.2.9</Version>
<AssemblyVersion>1.2.3.0</AssemblyVersion>
<FileVersion>1.2.3.0</FileVersion>
<InformationalVersion>1.2.3</InformationalVersion>
<Version>1.3.2</Version>
<AssemblyVersion>1.3.2.0</AssemblyVersion>
<FileVersion>1.3.2.0</FileVersion>
<InformationalVersion>1.3.2</InformationalVersion>
</PropertyGroup>
<ItemGroup>