diff --git a/config/ocsentinel-client.dev.example.json b/config/ocsentinel-client.dev.example.json
index 3cbc234..8ac85b5 100644
--- a/config/ocsentinel-client.dev.example.json
+++ b/config/ocsentinel-client.dev.example.json
@@ -4,6 +4,12 @@
"lookbackDays": 7,
"topFindings": 10,
"n8nWebhookUrl": "http://172.16.41.197:5678/webhook/ocsentinel-ingest",
+ "ninjaOrganizationId": "",
+ "ninjaOrganizationName": "",
+ "ninjaMachineId": "",
+ "ninjaNodeId": "",
+ "ninjaLocationId": "",
+ "ninjaLocationName": "",
"deviceIdentifierMode": "machineName",
"uploadTimeoutSeconds": 30,
"uploadQueueMaxReports": 100,
diff --git a/config/ocsentinel-client.example.json b/config/ocsentinel-client.example.json
index 04335c2..36836a6 100644
--- a/config/ocsentinel-client.example.json
+++ b/config/ocsentinel-client.example.json
@@ -4,6 +4,12 @@
"lookbackDays": 7,
"topFindings": 10,
"n8nWebhookUrl": "https://n8n.example.com/webhook/ocsentinel-ingest",
+ "ninjaOrganizationId": "",
+ "ninjaOrganizationName": "",
+ "ninjaMachineId": "",
+ "ninjaNodeId": "",
+ "ninjaLocationId": "",
+ "ninjaLocationName": "",
"deviceIdentifierMode": "machineName",
"uploadTimeoutSeconds": 30,
"uploadQueueMaxReports": 100,
diff --git a/docs/ocsentinel-deployment.md b/docs/ocsentinel-deployment.md
index 92c6ca2..2410d5c 100644
--- a/docs/ocsentinel-deployment.md
+++ b/docs/ocsentinel-deployment.md
@@ -32,6 +32,11 @@ powershell -ExecutionPolicy Bypass -File .\build\build-release-manifest.ps1 `
## Local Schedule And Burst Mode
+During a NinjaOne installation or update, OCSentinel stores the device's
+NinjaOne organization, location, and device identifiers in its local client
+configuration. Scheduled `SYSTEM` scans restore that context before creating a
+report, so their uploads remain assigned to the correct organization.
+
The installer creates two Windows Scheduled Tasks running as `SYSTEM`:
- `OCSentinel Daily Scan`: runs once per day and uploads one signed report.
diff --git a/installer/runtime-run-ocsentinel.ps1 b/installer/runtime-run-ocsentinel.ps1
index 07d837f..8a820d2 100644
--- a/installer/runtime-run-ocsentinel.ps1
+++ b/installer/runtime-run-ocsentinel.ps1
@@ -39,6 +39,40 @@ function Resolve-PathLike {
return [System.IO.Path]::GetFullPath((Join-Path $BasePath $PathValue))
}
+function Restore-NinjaContextFromClientConfiguration {
+ param([Parameter(Mandatory)][string]$Path)
+
+ if (-not (Test-Path -LiteralPath $Path)) {
+ return
+ }
+
+ try {
+ $clientConfiguration = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
+ $mappings = @(
+ @{ 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 ($mapping in $mappings) {
+ if (-not [string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($mapping.EnvironmentName, "Process"))) {
+ continue
+ }
+
+ $value = [string]$clientConfiguration.($mapping.PropertyName)
+ if (-not [string]::IsNullOrWhiteSpace($value)) {
+ [Environment]::SetEnvironmentVariable($mapping.EnvironmentName, $value, "Process")
+ }
+ }
+ }
+ catch {
+ Write-Warning "Could not restore stored NinjaOne context: $($_.Exception.Message)"
+ }
+}
+
if (-not (Test-Path $appExe)) {
throw "Application executable not found: $appExe"
}
@@ -54,6 +88,8 @@ else {
$secretFullPath = if ([string]::IsNullOrWhiteSpace($SecretPath)) { "" } else { Resolve-PathLike -PathValue $SecretPath -BasePath $scriptDir }
$canUpload = (Test-Path $clientConfigFullPath) -and (-not [string]::IsNullOrWhiteSpace($secretFullPath)) -and (Test-Path $secretFullPath)
+ Restore-NinjaContextFromClientConfiguration -Path $clientConfigFullPath
+
if ($UploadMode -eq "required" -and -not $canUpload) {
throw "UploadMode 'required' was set, but client config or protected secret is missing."
}
diff --git a/scripts/bootstrap-ocsentinel-ninja.ps1 b/scripts/bootstrap-ocsentinel-ninja.ps1
index cdd3160..5372e4e 100644
--- a/scripts/bootstrap-ocsentinel-ninja.ps1
+++ b/scripts/bootstrap-ocsentinel-ninja.ps1
@@ -164,8 +164,22 @@ if (-not [string]::IsNullOrWhiteSpace($WebhookUrl)) {
$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."
+ Write-Host "Configured OCSentinel upload endpoint and NinjaOne context."
}
if (-not [string]::IsNullOrWhiteSpace($SecretValue)) {
diff --git a/scripts/run-ocsentinel.ps1 b/scripts/run-ocsentinel.ps1
index 4409cf0..cbbcf9b 100644
--- a/scripts/run-ocsentinel.ps1
+++ b/scripts/run-ocsentinel.ps1
@@ -40,6 +40,40 @@ function Resolve-PathLike {
return [System.IO.Path]::GetFullPath((Join-Path $BasePath $PathValue))
}
+function Restore-NinjaContextFromClientConfiguration {
+ param([Parameter(Mandatory)][string]$Path)
+
+ if (-not (Test-Path -LiteralPath $Path)) {
+ return
+ }
+
+ try {
+ $clientConfiguration = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
+ $mappings = @(
+ @{ 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 ($mapping in $mappings) {
+ if (-not [string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($mapping.EnvironmentName, "Process"))) {
+ continue
+ }
+
+ $value = [string]$clientConfiguration.($mapping.PropertyName)
+ if (-not [string]::IsNullOrWhiteSpace($value)) {
+ [Environment]::SetEnvironmentVariable($mapping.EnvironmentName, $value, "Process")
+ }
+ }
+ }
+ catch {
+ Write-Warning "Could not restore stored NinjaOne context: $($_.Exception.Message)"
+ }
+}
+
$arguments = @(
$dllPath
)
@@ -53,6 +87,8 @@ else {
$secretFullPath = if ([string]::IsNullOrWhiteSpace($SecretPath)) { "" } else { Resolve-PathLike -PathValue $SecretPath -BasePath $repoRoot }
$canUpload = (Test-Path $clientConfigFullPath) -and (-not [string]::IsNullOrWhiteSpace($secretFullPath)) -and (Test-Path $secretFullPath)
+ Restore-NinjaContextFromClientConfiguration -Path $clientConfigFullPath
+
if ($UploadMode -eq "required" -and -not $canUpload) {
throw "UploadMode 'required' was set, but client config or protected secret is missing."
}
diff --git a/src/OCSentinelCli/OCSentinelCli.csproj b/src/OCSentinelCli/OCSentinelCli.csproj
index 441d0b9..e63f5a6 100644
--- a/src/OCSentinelCli/OCSentinelCli.csproj
+++ b/src/OCSentinelCli/OCSentinelCli.csproj
@@ -9,10 +9,10 @@
OCSentinelCli
OfficeCom Sentinel
OfficeCom
- 1.3.5
- 1.3.5.0
- 1.3.5.0
- 1.3.5
+ 1.3.6
+ 1.3.6.0
+ 1.3.6.0
+ 1.3.6