Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c2170090e | ||
|
|
91c5794502 | ||
|
|
f392057535 | ||
|
|
7431c656d3 | ||
|
|
49b0e3025d | ||
|
|
aefec51581 | ||
|
|
7555e92aac | ||
|
|
b97b554f84 | ||
|
|
77d7eaa0b5 | ||
|
|
f91f45ad92 | ||
|
|
c27b53ea0d | ||
|
|
f9d7647046 | ||
|
|
d37fba137e | ||
|
|
e73d79b520 | ||
|
|
b0e3e7dc74 | ||
|
|
290033680b | ||
|
|
3ea0baa147 | ||
|
|
e01aa3dce3 | ||
|
|
8bc1d78fc9 | ||
|
|
00778175fd | ||
|
|
34eff4e012 |
@@ -33,8 +33,9 @@ jobs:
|
|||||||
|
|
||||||
build-client-windows:
|
build-client-windows:
|
||||||
needs: validate-client
|
needs: validate-client
|
||||||
if: startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && inputs.build_windows == 'true')
|
# .NET can publish a self-contained Windows x64 client from Linux.
|
||||||
runs-on: windows
|
# This keeps releases independent of a Windows Gitea runner.
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -50,10 +51,14 @@ jobs:
|
|||||||
./build/build-client-package.ps1
|
./build/build-client-package.ps1
|
||||||
|
|
||||||
- name: Build release manifest for tags
|
- name: Build release manifest for tags
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
$tag = "${{ github.ref_name }}"
|
$ref = if ($env:GITHUB_REF) { $env:GITHUB_REF } else { $env:GITEA_REF }
|
||||||
|
$tag = if ($env:GITHUB_REF_NAME) { $env:GITHUB_REF_NAME } else { Split-Path -Leaf $ref }
|
||||||
|
if ($tag -notlike "v*") {
|
||||||
|
Write-Host "Not a version tag; skipping release manifest."
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
$artifactUrl = "https://gitea.officecom.cloud/officecom/oc-sentinel/releases/download/$tag/OCSentinelClient-win-x64.zip"
|
$artifactUrl = "https://gitea.officecom.cloud/officecom/oc-sentinel/releases/download/$tag/OCSentinelClient-win-x64.zip"
|
||||||
./build/build-release-manifest.ps1 -ArtifactUrl $artifactUrl
|
./build/build-release-manifest.ps1 -ArtifactUrl $artifactUrl
|
||||||
|
|
||||||
@@ -66,3 +71,41 @@ jobs:
|
|||||||
artifacts/OCSentinelClient-win-x64.zip.sha256
|
artifacts/OCSentinelClient-win-x64.zip.sha256
|
||||||
artifacts/version.json
|
artifacts/version.json
|
||||||
if-no-files-found: warn
|
if-no-files-found: warn
|
||||||
|
|
||||||
|
- name: Publish Gitea release assets
|
||||||
|
shell: pwsh
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
run: |
|
||||||
|
$ref = if ($env:GITHUB_REF) { $env:GITHUB_REF } else { $env:GITEA_REF }
|
||||||
|
$tag = if ($env:GITHUB_REF_NAME) { $env:GITHUB_REF_NAME } else { Split-Path -Leaf $ref }
|
||||||
|
if ($tag -notlike "v*") {
|
||||||
|
Write-Host "Not a version tag; skipping Gitea release publication."
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
$headers = @{ Authorization = "token $env:GITEA_TOKEN" }
|
||||||
|
$repository = if ($env:GITEA_REPOSITORY) { $env:GITEA_REPOSITORY } else { $env:GITHUB_REPOSITORY }
|
||||||
|
$serverUrl = if ($env:GITEA_SERVER_URL) { $env:GITEA_SERVER_URL } else { $env:GITHUB_SERVER_URL }
|
||||||
|
if ([string]::IsNullOrWhiteSpace($env:GITEA_TOKEN) -or [string]::IsNullOrWhiteSpace($repository) -or [string]::IsNullOrWhiteSpace($serverUrl)) {
|
||||||
|
throw "Gitea release environment is incomplete. Expected GITEA_TOKEN, repository, and server URL."
|
||||||
|
}
|
||||||
|
$baseUrl = "$serverUrl/api/v1/repos/$repository"
|
||||||
|
$releaseBody = @{
|
||||||
|
tag_name = $tag
|
||||||
|
target_commitish = "${{ github.sha }}"
|
||||||
|
name = "OfficeCom Sentinel $tag"
|
||||||
|
body = "Automated OfficeCom Sentinel client release."
|
||||||
|
} | ConvertTo-Json
|
||||||
|
|
||||||
|
try {
|
||||||
|
$release = Invoke-RestMethod -Method Get -Headers $headers -Uri "$baseUrl/releases/tags/$tag"
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
$release = Invoke-RestMethod -Method Post -Headers $headers -ContentType "application/json" -Body $releaseBody -Uri "$baseUrl/releases"
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($file in @("artifacts/OCSentinelClient-win-x64.zip", "artifacts/OCSentinelClient-win-x64.zip.sha256", "artifacts/version.json")) {
|
||||||
|
$assetName = [System.IO.Path]::GetFileName($file)
|
||||||
|
Invoke-RestMethod -Method Post -Headers $headers -InFile $file -ContentType "application/octet-stream" -Uri "$baseUrl/releases/$($release.id)/assets?name=$assetName" | Out-Null
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 "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.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-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 "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
|
Copy-Item -Path (Join-Path $repoRoot "config\ocsentinel-settings.example.json") -Destination (Join-Path $packageRoot "config\ocsentinel-settings.example.json") -Force
|
||||||
|
|||||||
@@ -30,6 +30,22 @@ powershell -ExecutionPolicy Bypass -File .\build\build-release-manifest.ps1 `
|
|||||||
- `config\ocsentinel-settings.json`
|
- `config\ocsentinel-settings.json`
|
||||||
- `config\ocsentinel-client.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
|
## NinjaOne Tasks
|
||||||
|
|
||||||
Create a PowerShell script in NinjaOne named `OCSentinel - Installieren oder aktualisieren`.
|
Create a PowerShell script in NinjaOne named `OCSentinel - Installieren oder aktualisieren`.
|
||||||
|
|||||||
@@ -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,
|
`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.
|
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`,
|
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-Nonce`, `X-ATN-Version`, `X-ATN-Payload-SHA256`, or
|
||||||
`X-ATN-Signature` is missing; reject timestamps outside five minutes.
|
`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>
|
<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
|
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.
|
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`,
|
5. `Postgres`: upsert the device, insert a row in `ocsentinel.scan_report`,
|
||||||
|
|||||||
@@ -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 "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.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-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")) {
|
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
|
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 "NoModify" -Value 1 -Type DWord
|
||||||
Set-ItemProperty -Path $uninstallKey -Name "NoRepair" -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 "Installation complete."
|
||||||
Write-Host "Main path: $installRoot"
|
Write-Host "Main path: $installRoot"
|
||||||
Write-Host "Runner: $(Join-Path $scriptRoot 'run-ocsentinel.ps1')"
|
Write-Host "Runner: $(Join-Path $scriptRoot 'run-ocsentinel.ps1')"
|
||||||
Write-Host "Monitor: $(Join-Path $scriptRoot 'run-ocsentinel-monitor.ps1')"
|
Write-Host "Monitor: $(Join-Path $scriptRoot 'run-ocsentinel-monitor.ps1')"
|
||||||
Write-Host "Updater: $(Join-Path $scriptRoot 'update-ocsentinel.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."
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ param(
|
|||||||
[string]$UploadMode = "auto",
|
[string]$UploadMode = "auto",
|
||||||
[string]$VulnerabilityCsvPath = "",
|
[string]$VulnerabilityCsvPath = "",
|
||||||
[string]$MirrorRoot = "",
|
[string]$MirrorRoot = "",
|
||||||
|
[switch]$SuppressTriggerExit,
|
||||||
[ValidateSet("status", "attack-only", "cve-critical", "attack-plus-cve")]
|
[ValidateSet("status", "attack-only", "cve-critical", "attack-plus-cve")]
|
||||||
[string]$Mode = "status"
|
[string]$Mode = "status"
|
||||||
)
|
)
|
||||||
@@ -46,8 +47,13 @@ function Initialize-NinjaFieldWriter {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Get-Command -Name "Set-NinjaProperty" -ErrorAction SilentlyContinue) {
|
||||||
|
$script:NinjaFieldBackend = "powershell-modern"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (Get-Command -Name "Ninja-Property-Set" -ErrorAction SilentlyContinue) {
|
if (Get-Command -Name "Ninja-Property-Set" -ErrorAction SilentlyContinue) {
|
||||||
$script:NinjaFieldBackend = "powershell"
|
$script:NinjaFieldBackend = "powershell-legacy"
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,14 +70,20 @@ function Set-NinjaCustomFieldValue {
|
|||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
[string]$Name,
|
[string]$Name,
|
||||||
[AllowEmptyString()]
|
[AllowEmptyString()]
|
||||||
[string]$Value
|
[object]$Value,
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$Type
|
||||||
)
|
)
|
||||||
|
|
||||||
Initialize-NinjaFieldWriter
|
Initialize-NinjaFieldWriter
|
||||||
|
|
||||||
switch ($script:NinjaFieldBackend) {
|
switch ($script:NinjaFieldBackend) {
|
||||||
"powershell" {
|
"powershell-modern" {
|
||||||
Ninja-Property-Set $Name $Value | Out-Null
|
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
|
return $true
|
||||||
}
|
}
|
||||||
"cli" {
|
"cli" {
|
||||||
@@ -112,28 +124,28 @@ function Publish-NinjaCustomFields {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$fieldValues = [ordered]@{
|
$fieldValues = @(
|
||||||
"ocsentinelstatus" = [string]$Report.AlertState
|
[pscustomobject]@{ Name = "ocsentinelstatus"; Type = "Text"; Value = [string]$Report.AlertState }
|
||||||
"ocsentinelreason" = $Reason
|
[pscustomobject]@{ Name = "ocsentinelreason"; Type = "Text"; Value = $Reason }
|
||||||
"ocsentinelbasestatus" = [string]$Report.BaseAlertState
|
[pscustomobject]@{ Name = "ocsentinelbasestatus"; Type = "Text"; Value = [string]$Report.BaseAlertState }
|
||||||
"ocsentinelevents" = [string]([int]$Report.TotalEvents)
|
[pscustomobject]@{ Name = "ocsentinelevents"; Type = "Integer"; Value = [int]$Report.TotalEvents }
|
||||||
"ocsentineluniqueips" = [string]([int]$Report.UniqueIpCount)
|
[pscustomobject]@{ Name = "ocsentineluniqueips"; Type = "Integer"; Value = [int]$Report.UniqueIpCount }
|
||||||
"ocsentinelcvecritical" = [string]([int]$Report.VulnerabilityCorrelation.CriticalCount)
|
[pscustomobject]@{ Name = "ocsentinelcvecritical"; Type = "Integer"; Value = [int]$Report.VulnerabilityCorrelation.CriticalCount }
|
||||||
"ocsentinelcvetotal" = [string]([int]$Report.VulnerabilityCorrelation.TotalCount)
|
[pscustomobject]@{ Name = "ocsentinelcvetotal"; Type = "Integer"; Value = [int]$Report.VulnerabilityCorrelation.TotalCount }
|
||||||
"ocsentinelmode" = $Mode
|
[pscustomobject]@{ Name = "ocsentinelmode"; Type = "Text"; Value = $Mode }
|
||||||
"ocsentineltriggered" = $Triggered.ToString().ToLowerInvariant()
|
[pscustomobject]@{ Name = "ocsentineltriggered"; Type = "Checkbox"; Value = $Triggered }
|
||||||
"ocsentinellastscanutc" = $generatedAtUtc
|
[pscustomobject]@{ Name = "ocsentinellastscanutc"; Type = "DateTime"; Value = $generatedAtUtc }
|
||||||
}
|
)
|
||||||
|
|
||||||
$updated = 0
|
$updated = 0
|
||||||
foreach ($entry in $fieldValues.GetEnumerator()) {
|
foreach ($entry in $fieldValues) {
|
||||||
try {
|
try {
|
||||||
if (Set-NinjaCustomFieldValue -Name $entry.Key -Value $entry.Value) {
|
if (Set-NinjaCustomFieldValue -Name $entry.Name -Value $entry.Value -Type $entry.Type) {
|
||||||
$updated++
|
$updated++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch {
|
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 "Report: $outputFullPath"
|
||||||
Write-Host "Runner exit code: $runnerExitCode"
|
Write-Host "Runner exit code: $runnerExitCode"
|
||||||
|
|
||||||
if ($monitorTriggered) {
|
if ($monitorTriggered -and -not $SuppressTriggerExit) {
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
exit 0
|
exit $runnerExitCode
|
||||||
|
|||||||
50
installer/runtime-run-ocsentinel-scheduled.ps1
Normal file
50
installer/runtime-run-ocsentinel-scheduled.ps1
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,12 @@ $ErrorActionPreference = "Stop"
|
|||||||
$installRoot = Join-Path ${env:ProgramFiles} "OCSentinel"
|
$installRoot = Join-Path ${env:ProgramFiles} "OCSentinel"
|
||||||
$uninstallKey = "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\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) {
|
if (Test-Path $uninstallKey) {
|
||||||
Remove-Item -Path $uninstallKey -Force -Recurse
|
Remove-Item -Path $uninstallKey -Force -Recurse
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"channel": "stable",
|
"channel": "stable",
|
||||||
"version": "1.2.3",
|
"version": "1.3.2",
|
||||||
"publishedAtUtc": "2026-07-16T22:58:05.1732183Z",
|
"publishedAtUtc": "2026-07-26T00:23:31.3024295Z",
|
||||||
"artifactUrl": "https://gitea.officecom.cloud/officecom/oc-sentinel/releases/download/v1.2.3/OCSentinelClient-win-x64.zip",
|
"artifactUrl": "https://gitea.officecom.cloud/officecom/oc-sentinel/releases/download/v1.3.2/OCSentinelClient-win-x64.zip",
|
||||||
"sha256": "50c7da8ff9cb96852493d4d89ad32bfd004a99fcf94b6acc70f8070ceaf737f2",
|
"sha256": "dbfedf0890176873cef8bc91b9f7a6d6c3c8382ffadc972324b9900bad1153cc",
|
||||||
"minUpdaterVersion": "1.0.0"
|
"minUpdaterVersion": "1.0.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,20 @@ $clientConfigPath = Join-Path $installRoot "config\ocsentinel-client.json"
|
|||||||
$secretScriptPath = Join-Path $installRoot "scripts\protect-ocsentinel-secret.ps1"
|
$secretScriptPath = Join-Path $installRoot "scripts\protect-ocsentinel-secret.ps1"
|
||||||
$secretPath = "C:\ProgramData\OCSentinel\secrets\ocsentinel-upload-secret.dat"
|
$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 {
|
function Assert-ArtifactSignature {
|
||||||
param([Parameter(Mandatory)][string]$ExecutablePath)
|
param([Parameter(Mandatory)][string]$ExecutablePath)
|
||||||
|
|
||||||
@@ -108,7 +122,7 @@ if (-not [string]::IsNullOrWhiteSpace($SecretValue)) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($RunInitialStatusScan) {
|
if ($runInitialScan) {
|
||||||
if (-not (Test-Path -LiteralPath $monitorPath)) {
|
if (-not (Test-Path -LiteralPath $monitorPath)) {
|
||||||
throw "OCSentinel was installed, but the monitor script is missing."
|
throw "OCSentinel was installed, but the monitor script is missing."
|
||||||
}
|
}
|
||||||
|
|||||||
62
scripts/configure-ocsentinel-ninja.ps1
Normal file
62
scripts/configure-ocsentinel-ninja.ps1
Normal 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
|
||||||
74
scripts/install-ocsentinel-ninja-once.ps1
Normal file
74
scripts/install-ocsentinel-ninja-once.ps1
Normal 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
|
||||||
@@ -9,6 +9,7 @@ param(
|
|||||||
[string]$UploadMode = "auto",
|
[string]$UploadMode = "auto",
|
||||||
[string]$VulnerabilityCsvPath = "",
|
[string]$VulnerabilityCsvPath = "",
|
||||||
[string]$MirrorRoot = "",
|
[string]$MirrorRoot = "",
|
||||||
|
[switch]$SuppressTriggerExit,
|
||||||
[ValidateSet("status", "attack-only", "cve-critical", "attack-plus-cve")]
|
[ValidateSet("status", "attack-only", "cve-critical", "attack-plus-cve")]
|
||||||
[string]$Mode = "status"
|
[string]$Mode = "status"
|
||||||
)
|
)
|
||||||
@@ -46,8 +47,13 @@ function Initialize-NinjaFieldWriter {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Get-Command -Name "Set-NinjaProperty" -ErrorAction SilentlyContinue) {
|
||||||
|
$script:NinjaFieldBackend = "powershell-modern"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (Get-Command -Name "Ninja-Property-Set" -ErrorAction SilentlyContinue) {
|
if (Get-Command -Name "Ninja-Property-Set" -ErrorAction SilentlyContinue) {
|
||||||
$script:NinjaFieldBackend = "powershell"
|
$script:NinjaFieldBackend = "powershell-legacy"
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,14 +70,20 @@ function Set-NinjaCustomFieldValue {
|
|||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
[string]$Name,
|
[string]$Name,
|
||||||
[AllowEmptyString()]
|
[AllowEmptyString()]
|
||||||
[string]$Value
|
[object]$Value,
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$Type
|
||||||
)
|
)
|
||||||
|
|
||||||
Initialize-NinjaFieldWriter
|
Initialize-NinjaFieldWriter
|
||||||
|
|
||||||
switch ($script:NinjaFieldBackend) {
|
switch ($script:NinjaFieldBackend) {
|
||||||
"powershell" {
|
"powershell-modern" {
|
||||||
Ninja-Property-Set $Name $Value | Out-Null
|
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
|
return $true
|
||||||
}
|
}
|
||||||
"cli" {
|
"cli" {
|
||||||
@@ -112,28 +124,28 @@ function Publish-NinjaCustomFields {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$fieldValues = [ordered]@{
|
$fieldValues = @(
|
||||||
"ocsentinelstatus" = [string]$Report.AlertState
|
[pscustomobject]@{ Name = "ocsentinelstatus"; Type = "Text"; Value = [string]$Report.AlertState }
|
||||||
"ocsentinelreason" = $Reason
|
[pscustomobject]@{ Name = "ocsentinelreason"; Type = "Text"; Value = $Reason }
|
||||||
"ocsentinelbasestatus" = [string]$Report.BaseAlertState
|
[pscustomobject]@{ Name = "ocsentinelbasestatus"; Type = "Text"; Value = [string]$Report.BaseAlertState }
|
||||||
"ocsentinelevents" = [string]([int]$Report.TotalEvents)
|
[pscustomobject]@{ Name = "ocsentinelevents"; Type = "Integer"; Value = [int]$Report.TotalEvents }
|
||||||
"ocsentineluniqueips" = [string]([int]$Report.UniqueIpCount)
|
[pscustomobject]@{ Name = "ocsentineluniqueips"; Type = "Integer"; Value = [int]$Report.UniqueIpCount }
|
||||||
"ocsentinelcvecritical" = [string]([int]$Report.VulnerabilityCorrelation.CriticalCount)
|
[pscustomobject]@{ Name = "ocsentinelcvecritical"; Type = "Integer"; Value = [int]$Report.VulnerabilityCorrelation.CriticalCount }
|
||||||
"ocsentinelcvetotal" = [string]([int]$Report.VulnerabilityCorrelation.TotalCount)
|
[pscustomobject]@{ Name = "ocsentinelcvetotal"; Type = "Integer"; Value = [int]$Report.VulnerabilityCorrelation.TotalCount }
|
||||||
"ocsentinelmode" = $Mode
|
[pscustomobject]@{ Name = "ocsentinelmode"; Type = "Text"; Value = $Mode }
|
||||||
"ocsentineltriggered" = $Triggered.ToString().ToLowerInvariant()
|
[pscustomobject]@{ Name = "ocsentineltriggered"; Type = "Checkbox"; Value = $Triggered }
|
||||||
"ocsentinellastscanutc" = $generatedAtUtc
|
[pscustomobject]@{ Name = "ocsentinellastscanutc"; Type = "DateTime"; Value = $generatedAtUtc }
|
||||||
}
|
)
|
||||||
|
|
||||||
$updated = 0
|
$updated = 0
|
||||||
foreach ($entry in $fieldValues.GetEnumerator()) {
|
foreach ($entry in $fieldValues) {
|
||||||
try {
|
try {
|
||||||
if (Set-NinjaCustomFieldValue -Name $entry.Key -Value $entry.Value) {
|
if (Set-NinjaCustomFieldValue -Name $entry.Name -Value $entry.Value -Type $entry.Type) {
|
||||||
$updated++
|
$updated++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch {
|
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 "Report: $outputFullPath"
|
||||||
Write-Host "Runner exit code: $runnerExitCode"
|
Write-Host "Runner exit code: $runnerExitCode"
|
||||||
|
|
||||||
if ($monitorTriggered) {
|
if ($monitorTriggered -and -not $SuppressTriggerExit) {
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
exit 0
|
exit $runnerExitCode
|
||||||
|
|||||||
50
scripts/run-ocsentinel-scheduled.ps1
Normal file
50
scripts/run-ocsentinel-scheduled.ps1
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -66,6 +66,7 @@ internal sealed class AttackScanner
|
|||||||
{
|
{
|
||||||
SchemaVersion = "2.0",
|
SchemaVersion = "2.0",
|
||||||
MachineName = Environment.MachineName,
|
MachineName = Environment.MachineName,
|
||||||
|
NinjaOne = GetNinjaOneContext(),
|
||||||
GeneratedAtLocal = generatedAtLocal,
|
GeneratedAtLocal = generatedAtLocal,
|
||||||
GeneratedAtUtc = generatedAtUtc,
|
GeneratedAtUtc = generatedAtUtc,
|
||||||
ClientVersion = BuildMetadata.Version,
|
ClientVersion = BuildMetadata.Version,
|
||||||
@@ -89,6 +90,24 @@ internal sealed class AttackScanner
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static NinjaOneContext GetNinjaOneContext()
|
||||||
|
{
|
||||||
|
return new NinjaOneContext
|
||||||
|
{
|
||||||
|
OrganizationId = ReadEnvironmentVariable("NINJA_ORGANIZATION_ID"),
|
||||||
|
OrganizationName = ReadEnvironmentVariable("NINJA_ORGANIZATION_NAME"),
|
||||||
|
MachineId = ReadEnvironmentVariable("NINJA_AGENT_MACHINE_ID"),
|
||||||
|
NodeId = ReadEnvironmentVariable("NINJA_AGENT_NODE_ID"),
|
||||||
|
LocationId = ReadEnvironmentVariable("NINJA_LOCATION_ID"),
|
||||||
|
LocationName = ReadEnvironmentVariable("NINJA_LOCATION_NAME")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReadEnvironmentVariable(string name)
|
||||||
|
{
|
||||||
|
return Environment.GetEnvironmentVariable(name)?.Trim() ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
private static ScannerConfiguration LoadConfiguration(ScanOptions options)
|
private static ScannerConfiguration LoadConfiguration(ScanOptions options)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(options.ConfigPath))
|
if (string.IsNullOrWhiteSpace(options.ConfigPath))
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ internal static class ScanAndUploadCommand
|
|||||||
public static int Execute(string[] args)
|
public static int Execute(string[] args)
|
||||||
{
|
{
|
||||||
string outputPath = @"C:\ProgramData\OCSentinel\reports\latest.json";
|
string outputPath = @"C:\ProgramData\OCSentinel\reports\latest.json";
|
||||||
|
string? clientConfigPath = null;
|
||||||
|
string? secretPath = null;
|
||||||
bool hasOutput = false;
|
bool hasOutput = false;
|
||||||
|
var scanArgs = new List<string>();
|
||||||
|
|
||||||
for (int i = 0; i < args.Length; i++)
|
for (int i = 0; i < args.Length; i++)
|
||||||
{
|
{
|
||||||
@@ -13,11 +16,26 @@ internal static class ScanAndUploadCommand
|
|||||||
{
|
{
|
||||||
outputPath = args[i + 1];
|
outputPath = args[i + 1];
|
||||||
hasOutput = true;
|
hasOutput = true;
|
||||||
break;
|
scanArgs.Add(args[i]);
|
||||||
}
|
scanArgs.Add(args[++i]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(args[i], "--client-config", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
|
||||||
|
{
|
||||||
|
clientConfigPath = args[++i];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(args[i], "--secret-path", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
|
||||||
|
{
|
||||||
|
secretPath = args[++i];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
scanArgs.Add(args[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<string> scanArgs = [.. args];
|
|
||||||
if (!hasOutput)
|
if (!hasOutput)
|
||||||
{
|
{
|
||||||
scanArgs.Add("--output");
|
scanArgs.Add("--output");
|
||||||
@@ -36,6 +54,16 @@ internal static class ScanAndUploadCommand
|
|||||||
outputPath
|
outputPath
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(clientConfigPath))
|
||||||
|
{
|
||||||
|
uploadArgs.AddRange(["--client-config", clientConfigPath]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(secretPath))
|
||||||
|
{
|
||||||
|
uploadArgs.AddRange(["--secret-path", secretPath]);
|
||||||
|
}
|
||||||
|
|
||||||
return UploadCommand.Execute([.. uploadArgs]);
|
return UploadCommand.Execute([.. uploadArgs]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ internal static class JsonOptions
|
|||||||
public static readonly JsonSerializerOptions Default = new()
|
public static readonly JsonSerializerOptions Default = new()
|
||||||
{
|
{
|
||||||
WriteIndented = true,
|
WriteIndented = true,
|
||||||
|
// Client configuration is also written by PowerShell/NinjaOne scripts.
|
||||||
|
// Accept their conventional camelCase names (for example n8nWebhookUrl).
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,9 @@ internal sealed record ScanResult
|
|||||||
|
|
||||||
public string MachineName { get; init; } = string.Empty;
|
public string MachineName { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
// Populated only for runs launched by NinjaOne automation.
|
||||||
|
public NinjaOneContext NinjaOne { get; init; } = new();
|
||||||
|
|
||||||
public DateTimeOffset GeneratedAtLocal { get; init; }
|
public DateTimeOffset GeneratedAtLocal { get; init; }
|
||||||
|
|
||||||
public DateTimeOffset GeneratedAtUtc { get; init; }
|
public DateTimeOffset GeneratedAtUtc { get; init; }
|
||||||
@@ -111,6 +114,21 @@ internal sealed record ScanResult
|
|||||||
public List<string> Errors { get; init; } = [];
|
public List<string> Errors { get; init; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal sealed record NinjaOneContext
|
||||||
|
{
|
||||||
|
public string OrganizationId { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string OrganizationName { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string MachineId { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string NodeId { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string LocationId { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string LocationName { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
internal sealed record ScanRuntimeMetadata
|
internal sealed record ScanRuntimeMetadata
|
||||||
{
|
{
|
||||||
public DateTimeOffset StartedAtUtc { get; init; }
|
public DateTimeOffset StartedAtUtc { get; init; }
|
||||||
|
|||||||
@@ -9,10 +9,10 @@
|
|||||||
<RootNamespace>OCSentinelCli</RootNamespace>
|
<RootNamespace>OCSentinelCli</RootNamespace>
|
||||||
<Product>OfficeCom Sentinel</Product>
|
<Product>OfficeCom Sentinel</Product>
|
||||||
<Company>OfficeCom</Company>
|
<Company>OfficeCom</Company>
|
||||||
<Version>1.2.4</Version>
|
<Version>1.3.2</Version>
|
||||||
<AssemblyVersion>1.2.3.0</AssemblyVersion>
|
<AssemblyVersion>1.3.2.0</AssemblyVersion>
|
||||||
<FileVersion>1.2.3.0</FileVersion>
|
<FileVersion>1.3.2.0</FileVersion>
|
||||||
<InformationalVersion>1.2.3</InformationalVersion>
|
<InformationalVersion>1.3.2</InformationalVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
Reference in New Issue
Block a user