Persist NinjaOne context for scheduled scans
This commit is contained in:
@@ -112,6 +112,28 @@ Runtime:
|
|||||||
-OutputPath "..\reports\ocsentinel-summary.json"
|
-OutputPath "..\reports\ocsentinel-summary.json"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Repair Missing NinjaOne Context
|
||||||
|
|
||||||
|
NinjaOne exposes `NINJA_ORGANIZATION_ID`, `NINJA_ORGANIZATION_NAME`,
|
||||||
|
`NINJA_AGENT_MACHINE_ID`, and location values only while an automation runs.
|
||||||
|
The scheduled OCSentinel task runs later as `SYSTEM`, so those values must be
|
||||||
|
persisted during an actual NinjaOne automation.
|
||||||
|
|
||||||
|
If the console shows `Organisation unbekannt`, create a temporary NinjaOne
|
||||||
|
PowerShell automation named `OCSentinel - NinjaOne Kontext aktualisieren` and
|
||||||
|
copy `scripts/refresh-ocsentinel-ninja-context.ps1` into the editor. Run it as
|
||||||
|
`SYSTEM` in 64-bit PowerShell once against the affected devices or policy.
|
||||||
|
|
||||||
|
The script has no script variables and does the following safely:
|
||||||
|
|
||||||
|
1. updates the installed client through the stable, version-independent manifest;
|
||||||
|
2. stores the current NinjaOne organization, location, node, and machine values;
|
||||||
|
3. starts one signed status scan and upload using that stored context.
|
||||||
|
|
||||||
|
Expected output includes `OCSENTINEL_NINJA_CONTEXT=updated`. Do not run this
|
||||||
|
script from an interactive PowerShell session, because NinjaOne does not expose
|
||||||
|
the required environment values there.
|
||||||
|
|
||||||
## Secret Bootstrap
|
## Secret Bootstrap
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
|
|||||||
@@ -44,6 +44,20 @@ foreach ($path in @($configPath, $secretScript, $monitorScript)) {
|
|||||||
$config = Get-Content -LiteralPath $configPath -Raw | ConvertFrom-Json
|
$config = Get-Content -LiteralPath $configPath -Raw | ConvertFrom-Json
|
||||||
$config.n8nWebhookUrl = $WebhookUrl
|
$config.n8nWebhookUrl = $WebhookUrl
|
||||||
$config.environment = "production"
|
$config.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)) {
|
||||||
|
$config | Add-Member -NotePropertyName $entry.PropertyName -NotePropertyValue $value.Trim() -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
$config | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $configPath -Encoding UTF8
|
$config | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $configPath -Encoding UTF8
|
||||||
Write-Host "OCSentinel upload endpoint configured."
|
Write-Host "OCSentinel upload endpoint configured."
|
||||||
|
|
||||||
|
|||||||
@@ -96,6 +96,20 @@ $secretPath = "C:\ProgramData\OCSentinel\secrets\ocsentinel-upload-secret.dat"
|
|||||||
$config = Get-Content -LiteralPath $configPath -Raw | ConvertFrom-Json
|
$config = Get-Content -LiteralPath $configPath -Raw | ConvertFrom-Json
|
||||||
$config.n8nWebhookUrl = $WebhookUrl
|
$config.n8nWebhookUrl = $WebhookUrl
|
||||||
$config.environment = "production"
|
$config.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)) {
|
||||||
|
$config | Add-Member -NotePropertyName $entry.PropertyName -NotePropertyValue $value.Trim() -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
$config | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $configPath -Encoding UTF8
|
$config | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $configPath -Encoding UTF8
|
||||||
|
|
||||||
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $secretScript -SecretValue $SecretValue
|
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $secretScript -SecretValue $SecretValue
|
||||||
|
|||||||
107
scripts/refresh-ocsentinel-ninja-context.ps1
Normal file
107
scripts/refresh-ocsentinel-ninja-context.ps1
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string]$ManifestUrl = "https://gitea.officecom.cloud/officecom/oc-sentinel/raw/main/release/stable/version.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
$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 Read-NinjaEnvironmentValue {
|
||||||
|
param([Parameter(Mandatory)][string]$Name)
|
||||||
|
|
||||||
|
$value = [Environment]::GetEnvironmentVariable($Name, "Process")
|
||||||
|
if ($null -eq $value) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value.Trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
$clientConfigPath = Join-Path $installRoot "config\ocsentinel-client.json"
|
||||||
|
$secretPath = "C:\ProgramData\OCSentinel\secrets\ocsentinel-upload-secret.dat"
|
||||||
|
|
||||||
|
foreach ($path in @($updaterPath, $clientConfigPath, $monitorPath)) {
|
||||||
|
if (-not (Test-Path -LiteralPath $path)) {
|
||||||
|
throw "OCSentinel installation is incomplete. Missing: $path"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# The NinjaOne context exists only during this script execution. Upgrade first so
|
||||||
|
# future scheduled scans restore the context from the local client configuration.
|
||||||
|
$escapedUpdaterPath = $updaterPath.Replace("'", "''")
|
||||||
|
$escapedManifestUrl = $ManifestUrl.Replace("'", "''")
|
||||||
|
$updateCommand = @"
|
||||||
|
`$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 '$escapedManifestUrl'
|
||||||
|
exit `$LASTEXITCODE
|
||||||
|
"@
|
||||||
|
& powershell.exe -NoProfile -ExecutionPolicy Bypass -Command $updateCommand | ForEach-Object { Write-Host $_ }
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "OCSentinel updater exited with code $LASTEXITCODE"
|
||||||
|
}
|
||||||
|
|
||||||
|
$mappings = @(
|
||||||
|
@{ EnvironmentName = "NINJA_ORGANIZATION_ID"; PropertyName = "ninjaOrganizationId"; Required = $true },
|
||||||
|
@{ EnvironmentName = "NINJA_ORGANIZATION_NAME"; PropertyName = "ninjaOrganizationName"; Required = $true },
|
||||||
|
@{ EnvironmentName = "NINJA_AGENT_MACHINE_ID"; PropertyName = "ninjaMachineId"; Required = $true },
|
||||||
|
@{ EnvironmentName = "NINJA_AGENT_NODE_ID"; PropertyName = "ninjaNodeId"; Required = $false },
|
||||||
|
@{ EnvironmentName = "NINJA_LOCATION_ID"; PropertyName = "ninjaLocationId"; Required = $false },
|
||||||
|
@{ EnvironmentName = "NINJA_LOCATION_NAME"; PropertyName = "ninjaLocationName"; Required = $false }
|
||||||
|
)
|
||||||
|
|
||||||
|
$clientConfig = Get-Content -LiteralPath $clientConfigPath -Raw | ConvertFrom-Json
|
||||||
|
$missing = @()
|
||||||
|
$captured = 0
|
||||||
|
foreach ($mapping in $mappings) {
|
||||||
|
$value = Read-NinjaEnvironmentValue -Name $mapping.EnvironmentName
|
||||||
|
if ([string]::IsNullOrWhiteSpace($value)) {
|
||||||
|
if ($mapping.Required) { $missing += $mapping.EnvironmentName }
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$clientConfig | Add-Member -NotePropertyName $mapping.PropertyName -NotePropertyValue $value -Force
|
||||||
|
$captured++
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($missing.Count -gt 0) {
|
||||||
|
throw "NinjaOne did not provide required context: $($missing -join ', '). Run this only from a NinjaOne automation, not from an interactive PowerShell session."
|
||||||
|
}
|
||||||
|
|
||||||
|
$clientConfig | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $clientConfigPath -Encoding UTF8
|
||||||
|
Write-Host "OCSentinel NinjaOne context captured: $captured of $($mappings.Count) values."
|
||||||
|
|
||||||
|
if ((Test-Path -LiteralPath $secretPath) -and -not [string]::IsNullOrWhiteSpace([string]$clientConfig.n8nWebhookUrl)) {
|
||||||
|
Write-Host "Running an immediate status scan and upload with the refreshed NinjaOne context."
|
||||||
|
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $monitorPath `
|
||||||
|
-Mode status `
|
||||||
|
-ClientConfigPath $clientConfigPath `
|
||||||
|
-SecretPath $secretPath `
|
||||||
|
-UploadMode required `
|
||||||
|
-SuppressTriggerExit
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "OCSentinel context refresh scan exited with code $LASTEXITCODE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Warning "Context was stored, but the upload configuration or protected secret is missing. The next configured scan will use the stored context."
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "OCSENTINEL_NINJA_CONTEXT=updated"
|
||||||
@@ -3,6 +3,7 @@ using System.Globalization;
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Runtime.Versioning;
|
using System.Runtime.Versioning;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using OCSentinelCli.Configuration;
|
||||||
|
|
||||||
namespace OCSentinelCli;
|
namespace OCSentinelCli;
|
||||||
|
|
||||||
@@ -68,7 +69,7 @@ internal sealed class AttackScanner
|
|||||||
{
|
{
|
||||||
SchemaVersion = "2.0",
|
SchemaVersion = "2.0",
|
||||||
MachineName = Environment.MachineName,
|
MachineName = Environment.MachineName,
|
||||||
NinjaOne = GetNinjaOneContext(),
|
NinjaOne = GetNinjaOneContext(options, errors),
|
||||||
GeneratedAtLocal = generatedAtLocal,
|
GeneratedAtLocal = generatedAtLocal,
|
||||||
GeneratedAtUtc = generatedAtUtc,
|
GeneratedAtUtc = generatedAtUtc,
|
||||||
ClientVersion = BuildMetadata.Version,
|
ClientVersion = BuildMetadata.Version,
|
||||||
@@ -93,19 +94,38 @@ internal sealed class AttackScanner
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static NinjaOneContext GetNinjaOneContext()
|
private static NinjaOneContext GetNinjaOneContext(ScanOptions options, List<string> errors)
|
||||||
{
|
{
|
||||||
|
ClientConfiguration? clientConfiguration = null;
|
||||||
|
if (!string.IsNullOrWhiteSpace(options.ClientConfigPath))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
clientConfiguration = ClientConfiguration.Load(options.ClientConfigPath);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
errors.Add($"Could not load persisted NinjaOne context: {exception.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return new NinjaOneContext
|
return new NinjaOneContext
|
||||||
{
|
{
|
||||||
OrganizationId = ReadEnvironmentVariable("NINJA_ORGANIZATION_ID"),
|
OrganizationId = ReadContextValue("NINJA_ORGANIZATION_ID", clientConfiguration?.NinjaOrganizationId),
|
||||||
OrganizationName = ReadEnvironmentVariable("NINJA_ORGANIZATION_NAME"),
|
OrganizationName = ReadContextValue("NINJA_ORGANIZATION_NAME", clientConfiguration?.NinjaOrganizationName),
|
||||||
MachineId = ReadEnvironmentVariable("NINJA_AGENT_MACHINE_ID"),
|
MachineId = ReadContextValue("NINJA_AGENT_MACHINE_ID", clientConfiguration?.NinjaMachineId),
|
||||||
NodeId = ReadEnvironmentVariable("NINJA_AGENT_NODE_ID"),
|
NodeId = ReadContextValue("NINJA_AGENT_NODE_ID", clientConfiguration?.NinjaNodeId),
|
||||||
LocationId = ReadEnvironmentVariable("NINJA_LOCATION_ID"),
|
LocationId = ReadContextValue("NINJA_LOCATION_ID", clientConfiguration?.NinjaLocationId),
|
||||||
LocationName = ReadEnvironmentVariable("NINJA_LOCATION_NAME")
|
LocationName = ReadContextValue("NINJA_LOCATION_NAME", clientConfiguration?.NinjaLocationName)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string ReadContextValue(string environmentName, string? persistedValue)
|
||||||
|
{
|
||||||
|
string currentValue = ReadEnvironmentVariable(environmentName);
|
||||||
|
return string.IsNullOrWhiteSpace(currentValue) ? persistedValue?.Trim() ?? string.Empty : currentValue;
|
||||||
|
}
|
||||||
|
|
||||||
private static string ReadEnvironmentVariable(string name)
|
private static string ReadEnvironmentVariable(string name)
|
||||||
{
|
{
|
||||||
return Environment.GetEnvironmentVariable(name)?.Trim() ?? string.Empty;
|
return Environment.GetEnvironmentVariable(name)?.Trim() ?? string.Empty;
|
||||||
|
|||||||
@@ -23,7 +23,10 @@ internal static class ScanAndUploadCommand
|
|||||||
|
|
||||||
if (string.Equals(args[i], "--client-config", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
|
if (string.Equals(args[i], "--client-config", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
|
||||||
{
|
{
|
||||||
clientConfigPath = args[++i];
|
string configPathValue = args[++i];
|
||||||
|
clientConfigPath = configPathValue;
|
||||||
|
scanArgs.Add("--client-config");
|
||||||
|
scanArgs.Add(configPathValue);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,18 @@ internal sealed record ClientConfiguration
|
|||||||
|
|
||||||
public string N8nWebhookUrl { get; init; } = string.Empty;
|
public string N8nWebhookUrl { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string NinjaOrganizationId { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string NinjaOrganizationName { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string NinjaMachineId { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string NinjaNodeId { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string NinjaLocationId { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string NinjaLocationName { get; init; } = string.Empty;
|
||||||
|
|
||||||
public string DeviceIdentifierMode { get; init; } = "machineName";
|
public string DeviceIdentifierMode { get; init; } = "machineName";
|
||||||
|
|
||||||
public int UploadTimeoutSeconds { get; init; } = 30;
|
public int UploadTimeoutSeconds { get; init; } = 30;
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ internal sealed record ScanOptions
|
|||||||
{
|
{
|
||||||
public const string Usage = """
|
public const string Usage = """
|
||||||
Usage:
|
Usage:
|
||||||
OCSentinelCli [--output <path>] [--lookback-days <n>] [--top <n>] [--config <path>] [--vulnerability-csv <path>] [--json-only] [--ninja-output] [--fail-on-attacks] [--fail-on-threshold] [--help]
|
OCSentinelCli [--output <path>] [--lookback-days <n>] [--top <n>] [--config <path>] [--client-config <path>] [--vulnerability-csv <path>] [--json-only] [--ninja-output] [--fail-on-attacks] [--fail-on-threshold] [--help]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--output <path> Write the JSON report to the given file.
|
--output <path> Write the JSON report to the given file.
|
||||||
--lookback-days <n> Only include events newer than now minus n days. Default: 30
|
--lookback-days <n> Only include events newer than now minus n days. Default: 30
|
||||||
--top <n> Number of aggregated source IPs to show. Default: 10
|
--top <n> Number of aggregated source IPs to show. Default: 10
|
||||||
--config <path> Load thresholds, path overrides, and exclusions from JSON.
|
--config <path> Load thresholds, path overrides, and exclusions from JSON.
|
||||||
|
--client-config <path> Load persisted NinjaOne identity and upload settings from JSON.
|
||||||
--vulnerability-csv <path>
|
--vulnerability-csv <path>
|
||||||
Correlate local attack results with exported CVE data for this host.
|
Correlate local attack results with exported CVE data for this host.
|
||||||
--json-only Print only JSON to stdout.
|
--json-only Print only JSON to stdout.
|
||||||
@@ -36,6 +37,8 @@ Options:
|
|||||||
|
|
||||||
public string? ConfigPath { get; init; }
|
public string? ConfigPath { get; init; }
|
||||||
|
|
||||||
|
public string? ClientConfigPath { get; init; }
|
||||||
|
|
||||||
public string? VulnerabilityCsvPath { get; init; }
|
public string? VulnerabilityCsvPath { get; init; }
|
||||||
|
|
||||||
public bool ShowHelp { get; init; }
|
public bool ShowHelp { get; init; }
|
||||||
@@ -72,6 +75,9 @@ Options:
|
|||||||
case "--config":
|
case "--config":
|
||||||
options = options with { ConfigPath = ReadValue(args, ref i, arg) };
|
options = options with { ConfigPath = ReadValue(args, ref i, arg) };
|
||||||
break;
|
break;
|
||||||
|
case "--client-config":
|
||||||
|
options = options with { ClientConfigPath = ReadValue(args, ref i, arg) };
|
||||||
|
break;
|
||||||
case "--vulnerability-csv":
|
case "--vulnerability-csv":
|
||||||
options = options with { VulnerabilityCsvPath = ReadValue(args, ref i, arg) };
|
options = options with { VulnerabilityCsvPath = ReadValue(args, ref i, arg) };
|
||||||
break;
|
break;
|
||||||
|
|||||||
Reference in New Issue
Block a user