From 85e394f647805c33ecc9a6b2559d93e25643cc10 Mon Sep 17 00:00:00 2001 From: OfficeCom Codex Date: Fri, 17 Jul 2026 00:50:26 +0200 Subject: [PATCH] Adopt OCSentinel script layout and add Gitea workflow --- .gitea/workflows/client-build.yml | 46 ++++ README.md | 38 ++++ build/build-client-package.ps1 | 12 +- docs/attacktracer-ninja-v2-deployment.md | 16 +- installer/install-attacktracer-ninja.ps1 | 82 +------ installer/install-ocsentinel.ps1 | 77 +++++++ installer/launch-install.cmd | 2 +- ...runtime-run-attacktracer-ninja-monitor.ps1 | 214 +----------------- installer/runtime-run-attacktracer-ninja.ps1 | 137 +---------- installer/runtime-run-ocsentinel-monitor.ps1 | 213 +++++++++++++++++ installer/runtime-run-ocsentinel.ps1 | 136 +++++++++++ installer/uninstall-attacktracer-ninja.ps1 | 21 +- installer/uninstall-ocsentinel.ps1 | 16 ++ installer/update-attacktracer-ninja.ps1 | 132 +---------- installer/update-ocsentinel.ps1 | 131 +++++++++++ scripts/protect-attacktracer-secret.ps1 | 26 +-- scripts/protect-ocsentinel-secret.ps1 | 25 ++ scripts/run-attacktracer-ninja-monitor.ps1 | 214 +----------------- scripts/run-attacktracer-ninja.ps1 | 145 +----------- scripts/run-ocsentinel-monitor.ps1 | 213 +++++++++++++++++ scripts/run-ocsentinel.ps1 | 144 ++++++++++++ .../Commands/ScanAndUploadCommand.cs | 2 +- .../Commands/UploadCommand.cs | 2 +- 23 files changed, 1124 insertions(+), 920 deletions(-) create mode 100644 .gitea/workflows/client-build.yml create mode 100644 README.md create mode 100644 installer/install-ocsentinel.ps1 create mode 100644 installer/runtime-run-ocsentinel-monitor.ps1 create mode 100644 installer/runtime-run-ocsentinel.ps1 create mode 100644 installer/uninstall-ocsentinel.ps1 create mode 100644 installer/update-ocsentinel.ps1 create mode 100644 scripts/protect-ocsentinel-secret.ps1 create mode 100644 scripts/run-ocsentinel-monitor.ps1 create mode 100644 scripts/run-ocsentinel.ps1 diff --git a/.gitea/workflows/client-build.yml b/.gitea/workflows/client-build.yml new file mode 100644 index 0000000..ff7eb0a --- /dev/null +++ b/.gitea/workflows/client-build.yml @@ -0,0 +1,46 @@ +name: OfficeCom Sentinel Client + +on: + push: + branches: + - main + tags: + - "v*" + workflow_dispatch: + +jobs: + build-client: + runs-on: + - self-hosted + - windows + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "10.0.x" + + - name: Build client package + shell: pwsh + run: | + ./build/build-client-package.ps1 + + - name: Build release manifest for tags + if: startsWith(github.ref, 'refs/tags/v') + shell: pwsh + run: | + $tag = "${{ github.ref_name }}" + $artifactUrl = "https://gitea.officecom.cloud/officecom/oc-sentinel/releases/download/$tag/OCSentinelClient-win-x64.zip" + ./build/build-release-manifest.ps1 -ArtifactUrl $artifactUrl + + - name: Upload package artifacts + uses: actions/upload-artifact@v4 + with: + name: ocsentinel-client-${{ github.sha }} + path: | + artifacts/OCSentinelClient-win-x64.zip + artifacts/OCSentinelClient-win-x64.zip.sha256 + artifacts/version.json + if-no-files-found: warn diff --git a/README.md b/README.md new file mode 100644 index 0000000..2d65c77 --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +# OfficeCom Sentinel + +OfficeCom Sentinel is the hardened endpoint client for Windows event correlation, NinjaOne field updates, CVE correlation, and optional n8n upload. + +## Main Paths + +- CLI source: `src/AttackTracerNinjaCli` +- local runner: `scripts/run-ocsentinel.ps1` +- Ninja monitor wrapper: `scripts/run-ocsentinel-monitor.ps1` +- packaged installer runtime: `installer/runtime-run-ocsentinel.ps1` +- package builder: `build/build-client-package.ps1` +- update manifest builder: `build/build-release-manifest.ps1` + +## Build + +```powershell +powershell -ExecutionPolicy Bypass -File .\build\build-client-package.ps1 +``` + +This creates: + +- `artifacts/OCSentinelClient-win-x64.zip` +- `artifacts/OCSentinelClient-win-x64.zip.sha256` + +## Release Manifest + +```powershell +powershell -ExecutionPolicy Bypass -File .\build\build-release-manifest.ps1 ` + -ArtifactUrl "https://gitea.officecom.cloud/officecom/oc-sentinel/releases/download/v2.0.0/OCSentinelClient-win-x64.zip" +``` + +This creates: + +- `artifacts/version.json` + +## Compatibility + +Legacy `attacktracer` script names are still present as wrappers so existing NinjaOne jobs do not break immediately. New work should use the `ocsentinel` script names. diff --git a/build/build-client-package.ps1 b/build/build-client-package.ps1 index b899ea2..e6c9d96 100644 --- a/build/build-client-package.ps1 +++ b/build/build-client-package.ps1 @@ -49,12 +49,12 @@ if (Test-Path (Join-Path $publishRoot "OCSentinelCli.pdb")) { Copy-Item -Path (Join-Path $publishRoot "OCSentinelCli.pdb") -Destination (Join-Path $packageRoot "app\OCSentinelCli.pdb") -Force } -Copy-Item -Path (Join-Path $installerRoot "install-attacktracer-ninja.ps1") -Destination (Join-Path $packageRoot "scripts\install-ocsentinel.ps1") -Force -Copy-Item -Path (Join-Path $installerRoot "uninstall-attacktracer-ninja.ps1") -Destination (Join-Path $packageRoot "scripts\uninstall-ocsentinel.ps1") -Force -Copy-Item -Path (Join-Path $installerRoot "update-attacktracer-ninja.ps1") -Destination (Join-Path $packageRoot "scripts\update-ocsentinel.ps1") -Force -Copy-Item -Path (Join-Path $installerRoot "runtime-run-attacktracer-ninja.ps1") -Destination (Join-Path $packageRoot "scripts\run-ocsentinel.ps1") -Force -Copy-Item -Path (Join-Path $installerRoot "runtime-run-attacktracer-ninja-monitor.ps1") -Destination (Join-Path $packageRoot "scripts\run-ocsentinel-monitor.ps1") -Force -Copy-Item -Path (Join-Path $repoRoot "scripts\protect-attacktracer-secret.ps1") -Destination (Join-Path $packageRoot "scripts\protect-ocsentinel-secret.ps1") -Force +Copy-Item -Path (Join-Path $installerRoot "install-ocsentinel.ps1") -Destination (Join-Path $packageRoot "scripts\install-ocsentinel.ps1") -Force +Copy-Item -Path (Join-Path $installerRoot "uninstall-ocsentinel.ps1") -Destination (Join-Path $packageRoot "scripts\uninstall-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-monitor.ps1") -Destination (Join-Path $packageRoot "scripts\run-ocsentinel-monitor.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\attacktracer-settings.example.json") -Destination (Join-Path $packageRoot "config\ocsentinel-settings.example.json") -Force Copy-Item -Path (Join-Path $repoRoot "config\attacktracer-client.example.json") -Destination (Join-Path $packageRoot "config\ocsentinel-client.example.json") -Force diff --git a/docs/attacktracer-ninja-v2-deployment.md b/docs/attacktracer-ninja-v2-deployment.md index 5f1d5c4..13991a0 100644 --- a/docs/attacktracer-ninja-v2-deployment.md +++ b/docs/attacktracer-ninja-v2-deployment.md @@ -1,12 +1,12 @@ -# AttackTracer Ninja V2 Deployment +# OfficeCom Sentinel Deployment ## Goal -Deploy and update the endpoint client through NinjaOne while hosting release artifacts in GitLab. +Deploy and update the endpoint client through NinjaOne while hosting release artifacts in Gitea. ## Release assets -Each GitLab release should publish: +Each Gitea release should publish: - `OCSentinelClient-win-x64.zip` - `OCSentinelClient-win-x64.zip.sha256` @@ -17,7 +17,7 @@ Build these locally with: ```powershell powershell -ExecutionPolicy Bypass -File .\build\build-client-package.ps1 powershell -ExecutionPolicy Bypass -File .\build\build-release-manifest.ps1 ` - -ArtifactUrl "https://gitlab.example.com/group/project/-/releases/v2.0.0/downloads/OCSentinelClient-win-x64.zip" + -ArtifactUrl "https://gitea.officecom.cloud/officecom/oc-sentinel/releases/download/v2.0.0/OCSentinelClient-win-x64.zip" ``` See example manifest: @@ -42,7 +42,7 @@ Recommended NinjaOne task: ```powershell & "C:\Program Files\OCSentinel\scripts\update-ocsentinel.ps1" ` - -ManifestUrl "https://gitlab.example.com/group/project/-/releases/permalink/latest/downloads/version.json" ` + -ManifestUrl "https://gitea.officecom.cloud/officecom/oc-sentinel/releases/download/v2.0.0/version.json" ` -Force ``` @@ -54,7 +54,7 @@ Recommended scheduled task command: ```powershell & "C:\Program Files\OCSentinel\scripts\update-ocsentinel.ps1" ` - -ManifestUrl "https://gitlab.example.com/group/project/-/releases/permalink/latest/downloads/version.json" + -ManifestUrl "https://gitea.officecom.cloud/officecom/oc-sentinel/releases/download/v2.0.0/version.json" ``` ## Behavior @@ -88,12 +88,12 @@ Recommended runtime task: ```powershell & "C:\Program Files\OCSentinel\scripts\run-ocsentinel-monitor.ps1" ` -Mode status ` - -OutputPath "..\reports\attacktracer-summary.json" + -OutputPath "..\reports\ocsentinel-summary.json" ``` Upload is enabled automatically when: -- `config\attacktracer-client.json` exists +- `config\ocsentinel-client.json` exists - the protected secret file exists Otherwise the client falls back to local scan behavior. diff --git a/installer/install-attacktracer-ninja.ps1 b/installer/install-attacktracer-ninja.ps1 index 2df014a..7fb1785 100644 --- a/installer/install-attacktracer-ninja.ps1 +++ b/installer/install-attacktracer-ninja.ps1 @@ -1,77 +1,15 @@ -param() +param( + [Parameter(ValueFromRemainingArguments = $true)] + [string[]]$RemainingArgs +) $ErrorActionPreference = "Stop" -$scriptPath = $MyInvocation.MyCommand.Path -$scriptDirectory = Split-Path -Parent $scriptPath -$packageRoot = if ((Split-Path -Leaf $scriptDirectory) -ieq "scripts") { Split-Path -Parent $scriptDirectory } else { $scriptDirectory } -$installRoot = Join-Path ${env:ProgramFiles} "OCSentinel" -$appRoot = Join-Path $installRoot "app" -$configRoot = Join-Path $installRoot "config" -$reportsRoot = Join-Path $installRoot "reports" -$samplesRoot = Join-Path $installRoot "samples" -$scriptRoot = Join-Path $installRoot "scripts" -$versionFile = Join-Path $packageRoot "VERSION.txt" -$version = if (Test-Path $versionFile) { (Get-Content $versionFile -Raw).Trim() } else { "1.0.0" } - -Write-Host "Installing OfficeCom Sentinel $version to $installRoot" - -New-Item -ItemType Directory -Force -Path $appRoot, $configRoot, $reportsRoot, $samplesRoot, $scriptRoot | Out-Null - -Copy-Item -Path (Join-Path $packageRoot "app\OCSentinelCli.exe") -Destination $appRoot -Force -if (Test-Path (Join-Path $packageRoot "app\OCSentinelCli.pdb")) { - Copy-Item -Path (Join-Path $packageRoot "app\OCSentinelCli.pdb") -Destination $appRoot -Force -} -Copy-Item -Path (Join-Path $packageRoot "config\ocsentinel-settings.example.json") -Destination (Join-Path $configRoot "ocsentinel-settings.example.json") -Force -if (Test-Path (Join-Path $packageRoot "config\ocsentinel-client.example.json")) { - Copy-Item -Path (Join-Path $packageRoot "config\ocsentinel-client.example.json") -Destination (Join-Path $configRoot "ocsentinel-client.example.json") -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-monitor.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 -} -if (Test-Path (Join-Path $packageRoot "scripts\update-ocsentinel.ps1")) { - Copy-Item -Path (Join-Path $packageRoot "scripts\update-ocsentinel.ps1") -Destination $scriptRoot -Force -} -if (Test-Path (Join-Path $packageRoot "scripts\build-attacktracer-org-report.ps1")) { - Copy-Item -Path (Join-Path $packageRoot "scripts\build-attacktracer-org-report.ps1") -Destination $scriptRoot -Force -} -Copy-Item -Path (Join-Path $packageRoot "scripts\uninstall-ocsentinel.ps1") -Destination $scriptRoot -Force - -$mainConfig = Join-Path $configRoot "ocsentinel-settings.json" -$exampleConfig = Join-Path $configRoot "ocsentinel-settings.example.json" -if (-not (Test-Path $mainConfig) -and (Test-Path $exampleConfig)) { - Copy-Item $exampleConfig $mainConfig -Force +$newScript = Join-Path $PSScriptRoot "install-ocsentinel.ps1" +if (-not (Test-Path $newScript)) { + throw "Replacement script not found: $newScript" } -$clientMainConfig = Join-Path $configRoot "ocsentinel-client.json" -$clientExampleConfig = Join-Path $configRoot "ocsentinel-client.example.json" -if (-not (Test-Path $clientMainConfig) -and (Test-Path $clientExampleConfig)) { - Copy-Item $clientExampleConfig $clientMainConfig -Force -} - -$uninstallScript = Join-Path $scriptRoot "uninstall-ocsentinel.ps1" -$uninstallCommand = "powershell.exe -ExecutionPolicy Bypass -File `"$uninstallScript`"" -$uninstallKey = "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\OCSentinel" - -if (-not (Test-Path $uninstallKey)) { - New-Item -Path $uninstallKey -Force | Out-Null -} - -Set-ItemProperty -Path $uninstallKey -Name "DisplayName" -Value "OfficeCom Sentinel" -Set-ItemProperty -Path $uninstallKey -Name "DisplayVersion" -Value $version -Set-ItemProperty -Path $uninstallKey -Name "Publisher" -Value "OfficeCom" -Set-ItemProperty -Path $uninstallKey -Name "InstallLocation" -Value $installRoot -Set-ItemProperty -Path $uninstallKey -Name "UninstallString" -Value $uninstallCommand -Set-ItemProperty -Path $uninstallKey -Name "QuietUninstallString" -Value $uninstallCommand -Set-ItemProperty -Path $uninstallKey -Name "NoModify" -Value 1 -Type DWord -Set-ItemProperty -Path $uninstallKey -Name "NoRepair" -Value 1 -Type DWord - -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 "Org report:$(Join-Path $scriptRoot 'build-attacktracer-org-report.ps1')" +Write-Host "Compatibility wrapper: install-attacktracer-ninja.ps1 -> install-ocsentinel.ps1" +& powershell.exe -ExecutionPolicy Bypass -File $newScript @RemainingArgs +exit $LASTEXITCODE diff --git a/installer/install-ocsentinel.ps1 b/installer/install-ocsentinel.ps1 new file mode 100644 index 0000000..2df014a --- /dev/null +++ b/installer/install-ocsentinel.ps1 @@ -0,0 +1,77 @@ +param() + +$ErrorActionPreference = "Stop" + +$scriptPath = $MyInvocation.MyCommand.Path +$scriptDirectory = Split-Path -Parent $scriptPath +$packageRoot = if ((Split-Path -Leaf $scriptDirectory) -ieq "scripts") { Split-Path -Parent $scriptDirectory } else { $scriptDirectory } +$installRoot = Join-Path ${env:ProgramFiles} "OCSentinel" +$appRoot = Join-Path $installRoot "app" +$configRoot = Join-Path $installRoot "config" +$reportsRoot = Join-Path $installRoot "reports" +$samplesRoot = Join-Path $installRoot "samples" +$scriptRoot = Join-Path $installRoot "scripts" +$versionFile = Join-Path $packageRoot "VERSION.txt" +$version = if (Test-Path $versionFile) { (Get-Content $versionFile -Raw).Trim() } else { "1.0.0" } + +Write-Host "Installing OfficeCom Sentinel $version to $installRoot" + +New-Item -ItemType Directory -Force -Path $appRoot, $configRoot, $reportsRoot, $samplesRoot, $scriptRoot | Out-Null + +Copy-Item -Path (Join-Path $packageRoot "app\OCSentinelCli.exe") -Destination $appRoot -Force +if (Test-Path (Join-Path $packageRoot "app\OCSentinelCli.pdb")) { + Copy-Item -Path (Join-Path $packageRoot "app\OCSentinelCli.pdb") -Destination $appRoot -Force +} +Copy-Item -Path (Join-Path $packageRoot "config\ocsentinel-settings.example.json") -Destination (Join-Path $configRoot "ocsentinel-settings.example.json") -Force +if (Test-Path (Join-Path $packageRoot "config\ocsentinel-client.example.json")) { + Copy-Item -Path (Join-Path $packageRoot "config\ocsentinel-client.example.json") -Destination (Join-Path $configRoot "ocsentinel-client.example.json") -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-monitor.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 +} +if (Test-Path (Join-Path $packageRoot "scripts\update-ocsentinel.ps1")) { + Copy-Item -Path (Join-Path $packageRoot "scripts\update-ocsentinel.ps1") -Destination $scriptRoot -Force +} +if (Test-Path (Join-Path $packageRoot "scripts\build-attacktracer-org-report.ps1")) { + Copy-Item -Path (Join-Path $packageRoot "scripts\build-attacktracer-org-report.ps1") -Destination $scriptRoot -Force +} +Copy-Item -Path (Join-Path $packageRoot "scripts\uninstall-ocsentinel.ps1") -Destination $scriptRoot -Force + +$mainConfig = Join-Path $configRoot "ocsentinel-settings.json" +$exampleConfig = Join-Path $configRoot "ocsentinel-settings.example.json" +if (-not (Test-Path $mainConfig) -and (Test-Path $exampleConfig)) { + Copy-Item $exampleConfig $mainConfig -Force +} + +$clientMainConfig = Join-Path $configRoot "ocsentinel-client.json" +$clientExampleConfig = Join-Path $configRoot "ocsentinel-client.example.json" +if (-not (Test-Path $clientMainConfig) -and (Test-Path $clientExampleConfig)) { + Copy-Item $clientExampleConfig $clientMainConfig -Force +} + +$uninstallScript = Join-Path $scriptRoot "uninstall-ocsentinel.ps1" +$uninstallCommand = "powershell.exe -ExecutionPolicy Bypass -File `"$uninstallScript`"" +$uninstallKey = "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\OCSentinel" + +if (-not (Test-Path $uninstallKey)) { + New-Item -Path $uninstallKey -Force | Out-Null +} + +Set-ItemProperty -Path $uninstallKey -Name "DisplayName" -Value "OfficeCom Sentinel" +Set-ItemProperty -Path $uninstallKey -Name "DisplayVersion" -Value $version +Set-ItemProperty -Path $uninstallKey -Name "Publisher" -Value "OfficeCom" +Set-ItemProperty -Path $uninstallKey -Name "InstallLocation" -Value $installRoot +Set-ItemProperty -Path $uninstallKey -Name "UninstallString" -Value $uninstallCommand +Set-ItemProperty -Path $uninstallKey -Name "QuietUninstallString" -Value $uninstallCommand +Set-ItemProperty -Path $uninstallKey -Name "NoModify" -Value 1 -Type DWord +Set-ItemProperty -Path $uninstallKey -Name "NoRepair" -Value 1 -Type DWord + +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 "Org report:$(Join-Path $scriptRoot 'build-attacktracer-org-report.ps1')" diff --git a/installer/launch-install.cmd b/installer/launch-install.cmd index 61a0b68..a5a2bde 100644 --- a/installer/launch-install.cmd +++ b/installer/launch-install.cmd @@ -1,3 +1,3 @@ @echo off -powershell.exe -ExecutionPolicy Bypass -File "%~dp0install-attacktracer-ninja.ps1" +powershell.exe -ExecutionPolicy Bypass -File "%~dp0install-ocsentinel.ps1" exit /b %errorlevel% diff --git a/installer/runtime-run-attacktracer-ninja-monitor.ps1 b/installer/runtime-run-attacktracer-ninja-monitor.ps1 index 5b43b58..9b01727 100644 --- a/installer/runtime-run-attacktracer-ninja-monitor.ps1 +++ b/installer/runtime-run-attacktracer-ninja-monitor.ps1 @@ -1,213 +1,15 @@ param( - [int]$LookbackDays = 7, - [int]$TopCount = 10, - [string]$OutputPath = "..\reports\ocsentinel-summary.json", - [string]$ConfigPath = "..\config\ocsentinel-settings.json", - [string]$VulnerabilityCsvPath = "", - [string]$MirrorRoot = "", - [ValidateSet("status", "attack-only", "cve-critical", "attack-plus-cve")] - [string]$Mode = "status" + [Parameter(ValueFromRemainingArguments = $true)] + [string[]]$RemainingArgs ) $ErrorActionPreference = "Stop" -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path -$runnerScript = Join-Path $scriptDir "run-ocsentinel.ps1" -$outputFullPath = [System.IO.Path]::GetFullPath((Join-Path $scriptDir $OutputPath)) - -$script:NinjaFieldBackend = $null -$script:NinjaCliPath = "C:\ProgramData\NinjaRMMAgent\ninjarmm-cli.exe" - -function Resolve-PathLike { - param( - [Parameter(Mandatory)] - [string]$PathValue, - [Parameter(Mandatory)] - [string]$BasePath - ) - - if ([string]::IsNullOrWhiteSpace($PathValue)) { - return $PathValue - } - - if ([System.IO.Path]::IsPathRooted($PathValue) -or $PathValue.StartsWith("\\")) { - return [System.IO.Path]::GetFullPath($PathValue) - } - - return [System.IO.Path]::GetFullPath((Join-Path $BasePath $PathValue)) +$newScript = Join-Path $PSScriptRoot "runtime-run-ocsentinel-monitor.ps1" +if (-not (Test-Path $newScript)) { + throw "Replacement script not found: $newScript" } -function Initialize-NinjaFieldWriter { - if ($null -ne $script:NinjaFieldBackend) { - return - } - - if (Get-Command -Name "Ninja-Property-Set" -ErrorAction SilentlyContinue) { - $script:NinjaFieldBackend = "powershell" - return - } - - if (Test-Path $script:NinjaCliPath) { - $script:NinjaFieldBackend = "cli" - return - } - - $script:NinjaFieldBackend = "none" -} - -function Set-NinjaCustomFieldValue { - param( - [Parameter(Mandatory)] - [string]$Name, - [AllowEmptyString()] - [string]$Value - ) - - Initialize-NinjaFieldWriter - - switch ($script:NinjaFieldBackend) { - "powershell" { - Ninja-Property-Set $Name $Value | Out-Null - return $true - } - "cli" { - & $script:NinjaCliPath set $Name $Value | Out-Null - return $LASTEXITCODE -eq 0 - } - default { - return $false - } - } -} - -function Publish-NinjaCustomFields { - param( - [Parameter(Mandatory)] - [pscustomobject]$Report, - [Parameter(Mandatory)] - [string]$Mode, - [Parameter(Mandatory)] - [bool]$Triggered, - [Parameter(Mandatory)] - [string]$Reason - ) - - Initialize-NinjaFieldWriter - if ($script:NinjaFieldBackend -eq "none") { - Write-Host "Ninja custom fields: skipped (Ninja field writer not available)." - return - } - - $generatedAtUtc = "" - if ($Report.GeneratedAtLocal) { - try { - $generatedAtUtc = ([DateTimeOffset]$Report.GeneratedAtLocal).ToUniversalTime().ToString("o") - } - catch { - $generatedAtUtc = [string]$Report.GeneratedAtLocal - } - } - - $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 - } - - $updated = 0 - foreach ($entry in $fieldValues.GetEnumerator()) { - try { - if (Set-NinjaCustomFieldValue -Name $entry.Key -Value $entry.Value) { - $updated++ - } - } - catch { - Write-Warning "Failed to set Ninja custom field '$($entry.Key)': $($_.Exception.Message)" - } - } - - Write-Host "Ninja custom fields: updated $updated field(s) via $script:NinjaFieldBackend." -} - -$runnerArgs = @( - "-ExecutionPolicy", "Bypass", - "-File", $runnerScript, - "-LookbackDays", $LookbackDays, - "-TopCount", $TopCount, - "-OutputPath", $OutputPath, - "-ConfigPath", $ConfigPath -) - -if (-not [string]::IsNullOrWhiteSpace($VulnerabilityCsvPath)) { - $runnerArgs += @("-VulnerabilityCsvPath", $VulnerabilityCsvPath) -} - -if (-not [string]::IsNullOrWhiteSpace($MirrorRoot)) { - $runnerArgs += @("-MirrorRoot", (Resolve-PathLike -PathValue $MirrorRoot -BasePath $scriptDir)) -} - -$null = & powershell @runnerArgs -$runnerExitCode = $LASTEXITCODE - -if (-not (Test-Path $outputFullPath)) { - throw "Expected report file was not created: $outputFullPath" -} - -$report = Get-Content $outputFullPath -Raw | ConvertFrom-Json - -$status = [string]$report.AlertState -$baseStatus = [string]$report.BaseAlertState -$events = [int]$report.TotalEvents -$uniqueIps = [int]$report.UniqueIpCount -$criticalCves = [int]$report.VulnerabilityCorrelation.CriticalCount -$totalCves = [int]$report.VulnerabilityCorrelation.TotalCount - -$monitorTriggered = $false -$monitorReason = "" - -switch ($Mode) { - "status" { - $monitorTriggered = $status -ne "ok" - $monitorReason = "Final status is $status. $($report.AlertReason)" - } - "attack-only" { - $monitorTriggered = $baseStatus -ne "ok" - $monitorReason = "Base attack status is $baseStatus. $($report.BaseAlertReason)" - } - "cve-critical" { - $monitorTriggered = $criticalCves -gt 0 - $monitorReason = "Critical/high CVE count is $criticalCves out of total CVEs $totalCves." - } - "attack-plus-cve" { - $monitorTriggered = ($events -gt 0 -and $criticalCves -gt 0) - $monitorReason = "Attack events=$events and critical/high CVEs=$criticalCves." - } -} - -Publish-NinjaCustomFields -Report $report -Mode $Mode -Triggered $monitorTriggered -Reason $monitorReason - -Write-Host "" -Write-Host "OfficeCom Sentinel monitor mode: $Mode" -Write-Host "Triggered: $monitorTriggered" -Write-Host "Reason: $monitorReason" -Write-Host "Status: $status" -Write-Host "Base status: $baseStatus" -Write-Host "Events: $events" -Write-Host "Unique IPs: $uniqueIps" -Write-Host "Critical/High CVEs: $criticalCves" -Write-Host "Total CVEs: $totalCves" -Write-Host "Report: $outputFullPath" -Write-Host "Runner exit code: $runnerExitCode" - -if ($monitorTriggered) { - exit 1 -} - -exit 0 +Write-Host "Compatibility wrapper: runtime-run-attacktracer-ninja-monitor.ps1 -> runtime-run-ocsentinel-monitor.ps1" +& powershell.exe -ExecutionPolicy Bypass -File $newScript @RemainingArgs +exit $LASTEXITCODE diff --git a/installer/runtime-run-attacktracer-ninja.ps1 b/installer/runtime-run-attacktracer-ninja.ps1 index 07d837f..7889dfa 100644 --- a/installer/runtime-run-attacktracer-ninja.ps1 +++ b/installer/runtime-run-attacktracer-ninja.ps1 @@ -1,136 +1,15 @@ param( - [int]$LookbackDays = 7, - [int]$TopCount = 10, - [string]$OutputPath = "..\reports\ocsentinel-summary.json", - [string]$ConfigPath = "..\config\ocsentinel-settings.json", - [string]$ClientConfigPath = "..\config\ocsentinel-client.json", - [string]$SecretPath = "", - [string]$VulnerabilityCsvPath = "", - [string]$MirrorRoot = "", - [ValidateSet("disabled", "auto", "required")] - [string]$UploadMode = "auto", - [switch]$FailOnAttacks, - [switch]$FailOnThreshold + [Parameter(ValueFromRemainingArguments = $true)] + [string[]]$RemainingArgs ) $ErrorActionPreference = "Stop" -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path -$installRoot = Split-Path -Parent $scriptDir -$appExe = Join-Path $installRoot "app\OCSentinelCli.exe" -$outputFullPath = [System.IO.Path]::GetFullPath((Join-Path $scriptDir $OutputPath)) - -function Resolve-PathLike { - param( - [Parameter(Mandatory)] - [string]$PathValue, - [Parameter(Mandatory)] - [string]$BasePath - ) - - if ([string]::IsNullOrWhiteSpace($PathValue)) { - return $PathValue - } - - if ([System.IO.Path]::IsPathRooted($PathValue) -or $PathValue.StartsWith("\\")) { - return [System.IO.Path]::GetFullPath($PathValue) - } - - return [System.IO.Path]::GetFullPath((Join-Path $BasePath $PathValue)) +$newScript = Join-Path $PSScriptRoot "runtime-run-ocsentinel.ps1" +if (-not (Test-Path $newScript)) { + throw "Replacement script not found: $newScript" } -if (-not (Test-Path $appExe)) { - throw "Application executable not found: $appExe" -} - -$arguments = @() - -$configFullPath = [System.IO.Path]::GetFullPath((Join-Path $scriptDir $ConfigPath)) -if ($UploadMode -eq "disabled") { - $arguments += "scan" -} -else { - $clientConfigFullPath = Resolve-PathLike -PathValue $ClientConfigPath -BasePath $scriptDir - $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) - - if ($UploadMode -eq "required" -and -not $canUpload) { - throw "UploadMode 'required' was set, but client config or protected secret is missing." - } - - if ($canUpload) { - $arguments += "scan-and-upload" - $arguments += @("--client-config", $clientConfigFullPath, "--secret-path", $secretFullPath) - } - else { - $arguments += "scan" - } -} - -$arguments += @( - "--lookback-days", $LookbackDays, - "--top", $TopCount, - "--output", $outputFullPath, - "--ninja-output" -) - -if (Test-Path $configFullPath) { - $arguments += @("--config", $configFullPath) -} - -if (-not [string]::IsNullOrWhiteSpace($VulnerabilityCsvPath)) { - $vulnerabilityCsvFullPath = Resolve-PathLike -PathValue $VulnerabilityCsvPath -BasePath $scriptDir - if (Test-Path $vulnerabilityCsvFullPath) { - $arguments += @("--vulnerability-csv", $vulnerabilityCsvFullPath) - } -} - -if ($FailOnAttacks) { - $arguments += "--fail-on-attacks" -} - -if ($FailOnThreshold) { - $arguments += "--fail-on-threshold" -} - -& $appExe @arguments -$exitCode = $LASTEXITCODE - -if (-not (Test-Path $outputFullPath)) { - throw "Expected report file was not created: $outputFullPath" -} - -$report = Get-Content $outputFullPath -Raw | ConvertFrom-Json - -if (-not [string]::IsNullOrWhiteSpace($MirrorRoot)) { - Write-Host "Legacy mirror mode enabled." - $mirrorRootPath = Resolve-PathLike -PathValue $MirrorRoot -BasePath $scriptDir - if (-not (Test-Path $mirrorRootPath)) { - New-Item -ItemType Directory -Force -Path $mirrorRootPath | Out-Null - } - $mirrorPath = Join-Path $mirrorRootPath "$($report.MachineName).json" - Copy-Item -Path $outputFullPath -Destination $mirrorPath -Force - Write-Host "Mirrored report: $mirrorPath" -} - -Write-Host "" -Write-Host "OfficeCom Sentinel runner summary" -Write-Host "Machine: $($report.MachineName)" -Write-Host "Events: $($report.TotalEvents)" -Write-Host "Unique IPs: $($report.UniqueIpCount)" -Write-Host "Status: $($report.AlertState)" -Write-Host "Reason: $($report.AlertReason)" -Write-Host "Base status: $($report.BaseAlertState)" -Write-Host "CVE findings: $($report.VulnerabilityCorrelation.TotalCount)" -Write-Host "Critical/High CVEs: $($report.VulnerabilityCorrelation.CriticalCount)" -Write-Host "Upload mode: $UploadMode" -Write-Host "Report: $outputFullPath" - -if ($report.Errors.Count -gt 0) { - Write-Host "Warnings:" - foreach ($warningEntry in $report.Errors) { - Write-Host "- $warningEntry" - } -} - -exit $exitCode +Write-Host "Compatibility wrapper: runtime-run-attacktracer-ninja.ps1 -> runtime-run-ocsentinel.ps1" +& powershell.exe -ExecutionPolicy Bypass -File $newScript @RemainingArgs +exit $LASTEXITCODE diff --git a/installer/runtime-run-ocsentinel-monitor.ps1 b/installer/runtime-run-ocsentinel-monitor.ps1 new file mode 100644 index 0000000..5b43b58 --- /dev/null +++ b/installer/runtime-run-ocsentinel-monitor.ps1 @@ -0,0 +1,213 @@ +param( + [int]$LookbackDays = 7, + [int]$TopCount = 10, + [string]$OutputPath = "..\reports\ocsentinel-summary.json", + [string]$ConfigPath = "..\config\ocsentinel-settings.json", + [string]$VulnerabilityCsvPath = "", + [string]$MirrorRoot = "", + [ValidateSet("status", "attack-only", "cve-critical", "attack-plus-cve")] + [string]$Mode = "status" +) + +$ErrorActionPreference = "Stop" + +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$runnerScript = Join-Path $scriptDir "run-ocsentinel.ps1" +$outputFullPath = [System.IO.Path]::GetFullPath((Join-Path $scriptDir $OutputPath)) + +$script:NinjaFieldBackend = $null +$script:NinjaCliPath = "C:\ProgramData\NinjaRMMAgent\ninjarmm-cli.exe" + +function Resolve-PathLike { + param( + [Parameter(Mandatory)] + [string]$PathValue, + [Parameter(Mandatory)] + [string]$BasePath + ) + + if ([string]::IsNullOrWhiteSpace($PathValue)) { + return $PathValue + } + + if ([System.IO.Path]::IsPathRooted($PathValue) -or $PathValue.StartsWith("\\")) { + return [System.IO.Path]::GetFullPath($PathValue) + } + + return [System.IO.Path]::GetFullPath((Join-Path $BasePath $PathValue)) +} + +function Initialize-NinjaFieldWriter { + if ($null -ne $script:NinjaFieldBackend) { + return + } + + if (Get-Command -Name "Ninja-Property-Set" -ErrorAction SilentlyContinue) { + $script:NinjaFieldBackend = "powershell" + return + } + + if (Test-Path $script:NinjaCliPath) { + $script:NinjaFieldBackend = "cli" + return + } + + $script:NinjaFieldBackend = "none" +} + +function Set-NinjaCustomFieldValue { + param( + [Parameter(Mandatory)] + [string]$Name, + [AllowEmptyString()] + [string]$Value + ) + + Initialize-NinjaFieldWriter + + switch ($script:NinjaFieldBackend) { + "powershell" { + Ninja-Property-Set $Name $Value | Out-Null + return $true + } + "cli" { + & $script:NinjaCliPath set $Name $Value | Out-Null + return $LASTEXITCODE -eq 0 + } + default { + return $false + } + } +} + +function Publish-NinjaCustomFields { + param( + [Parameter(Mandatory)] + [pscustomobject]$Report, + [Parameter(Mandatory)] + [string]$Mode, + [Parameter(Mandatory)] + [bool]$Triggered, + [Parameter(Mandatory)] + [string]$Reason + ) + + Initialize-NinjaFieldWriter + if ($script:NinjaFieldBackend -eq "none") { + Write-Host "Ninja custom fields: skipped (Ninja field writer not available)." + return + } + + $generatedAtUtc = "" + if ($Report.GeneratedAtLocal) { + try { + $generatedAtUtc = ([DateTimeOffset]$Report.GeneratedAtLocal).ToUniversalTime().ToString("o") + } + catch { + $generatedAtUtc = [string]$Report.GeneratedAtLocal + } + } + + $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 + } + + $updated = 0 + foreach ($entry in $fieldValues.GetEnumerator()) { + try { + if (Set-NinjaCustomFieldValue -Name $entry.Key -Value $entry.Value) { + $updated++ + } + } + catch { + Write-Warning "Failed to set Ninja custom field '$($entry.Key)': $($_.Exception.Message)" + } + } + + Write-Host "Ninja custom fields: updated $updated field(s) via $script:NinjaFieldBackend." +} + +$runnerArgs = @( + "-ExecutionPolicy", "Bypass", + "-File", $runnerScript, + "-LookbackDays", $LookbackDays, + "-TopCount", $TopCount, + "-OutputPath", $OutputPath, + "-ConfigPath", $ConfigPath +) + +if (-not [string]::IsNullOrWhiteSpace($VulnerabilityCsvPath)) { + $runnerArgs += @("-VulnerabilityCsvPath", $VulnerabilityCsvPath) +} + +if (-not [string]::IsNullOrWhiteSpace($MirrorRoot)) { + $runnerArgs += @("-MirrorRoot", (Resolve-PathLike -PathValue $MirrorRoot -BasePath $scriptDir)) +} + +$null = & powershell @runnerArgs +$runnerExitCode = $LASTEXITCODE + +if (-not (Test-Path $outputFullPath)) { + throw "Expected report file was not created: $outputFullPath" +} + +$report = Get-Content $outputFullPath -Raw | ConvertFrom-Json + +$status = [string]$report.AlertState +$baseStatus = [string]$report.BaseAlertState +$events = [int]$report.TotalEvents +$uniqueIps = [int]$report.UniqueIpCount +$criticalCves = [int]$report.VulnerabilityCorrelation.CriticalCount +$totalCves = [int]$report.VulnerabilityCorrelation.TotalCount + +$monitorTriggered = $false +$monitorReason = "" + +switch ($Mode) { + "status" { + $monitorTriggered = $status -ne "ok" + $monitorReason = "Final status is $status. $($report.AlertReason)" + } + "attack-only" { + $monitorTriggered = $baseStatus -ne "ok" + $monitorReason = "Base attack status is $baseStatus. $($report.BaseAlertReason)" + } + "cve-critical" { + $monitorTriggered = $criticalCves -gt 0 + $monitorReason = "Critical/high CVE count is $criticalCves out of total CVEs $totalCves." + } + "attack-plus-cve" { + $monitorTriggered = ($events -gt 0 -and $criticalCves -gt 0) + $monitorReason = "Attack events=$events and critical/high CVEs=$criticalCves." + } +} + +Publish-NinjaCustomFields -Report $report -Mode $Mode -Triggered $monitorTriggered -Reason $monitorReason + +Write-Host "" +Write-Host "OfficeCom Sentinel monitor mode: $Mode" +Write-Host "Triggered: $monitorTriggered" +Write-Host "Reason: $monitorReason" +Write-Host "Status: $status" +Write-Host "Base status: $baseStatus" +Write-Host "Events: $events" +Write-Host "Unique IPs: $uniqueIps" +Write-Host "Critical/High CVEs: $criticalCves" +Write-Host "Total CVEs: $totalCves" +Write-Host "Report: $outputFullPath" +Write-Host "Runner exit code: $runnerExitCode" + +if ($monitorTriggered) { + exit 1 +} + +exit 0 diff --git a/installer/runtime-run-ocsentinel.ps1 b/installer/runtime-run-ocsentinel.ps1 new file mode 100644 index 0000000..07d837f --- /dev/null +++ b/installer/runtime-run-ocsentinel.ps1 @@ -0,0 +1,136 @@ +param( + [int]$LookbackDays = 7, + [int]$TopCount = 10, + [string]$OutputPath = "..\reports\ocsentinel-summary.json", + [string]$ConfigPath = "..\config\ocsentinel-settings.json", + [string]$ClientConfigPath = "..\config\ocsentinel-client.json", + [string]$SecretPath = "", + [string]$VulnerabilityCsvPath = "", + [string]$MirrorRoot = "", + [ValidateSet("disabled", "auto", "required")] + [string]$UploadMode = "auto", + [switch]$FailOnAttacks, + [switch]$FailOnThreshold +) + +$ErrorActionPreference = "Stop" + +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$installRoot = Split-Path -Parent $scriptDir +$appExe = Join-Path $installRoot "app\OCSentinelCli.exe" +$outputFullPath = [System.IO.Path]::GetFullPath((Join-Path $scriptDir $OutputPath)) + +function Resolve-PathLike { + param( + [Parameter(Mandatory)] + [string]$PathValue, + [Parameter(Mandatory)] + [string]$BasePath + ) + + if ([string]::IsNullOrWhiteSpace($PathValue)) { + return $PathValue + } + + if ([System.IO.Path]::IsPathRooted($PathValue) -or $PathValue.StartsWith("\\")) { + return [System.IO.Path]::GetFullPath($PathValue) + } + + return [System.IO.Path]::GetFullPath((Join-Path $BasePath $PathValue)) +} + +if (-not (Test-Path $appExe)) { + throw "Application executable not found: $appExe" +} + +$arguments = @() + +$configFullPath = [System.IO.Path]::GetFullPath((Join-Path $scriptDir $ConfigPath)) +if ($UploadMode -eq "disabled") { + $arguments += "scan" +} +else { + $clientConfigFullPath = Resolve-PathLike -PathValue $ClientConfigPath -BasePath $scriptDir + $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) + + if ($UploadMode -eq "required" -and -not $canUpload) { + throw "UploadMode 'required' was set, but client config or protected secret is missing." + } + + if ($canUpload) { + $arguments += "scan-and-upload" + $arguments += @("--client-config", $clientConfigFullPath, "--secret-path", $secretFullPath) + } + else { + $arguments += "scan" + } +} + +$arguments += @( + "--lookback-days", $LookbackDays, + "--top", $TopCount, + "--output", $outputFullPath, + "--ninja-output" +) + +if (Test-Path $configFullPath) { + $arguments += @("--config", $configFullPath) +} + +if (-not [string]::IsNullOrWhiteSpace($VulnerabilityCsvPath)) { + $vulnerabilityCsvFullPath = Resolve-PathLike -PathValue $VulnerabilityCsvPath -BasePath $scriptDir + if (Test-Path $vulnerabilityCsvFullPath) { + $arguments += @("--vulnerability-csv", $vulnerabilityCsvFullPath) + } +} + +if ($FailOnAttacks) { + $arguments += "--fail-on-attacks" +} + +if ($FailOnThreshold) { + $arguments += "--fail-on-threshold" +} + +& $appExe @arguments +$exitCode = $LASTEXITCODE + +if (-not (Test-Path $outputFullPath)) { + throw "Expected report file was not created: $outputFullPath" +} + +$report = Get-Content $outputFullPath -Raw | ConvertFrom-Json + +if (-not [string]::IsNullOrWhiteSpace($MirrorRoot)) { + Write-Host "Legacy mirror mode enabled." + $mirrorRootPath = Resolve-PathLike -PathValue $MirrorRoot -BasePath $scriptDir + if (-not (Test-Path $mirrorRootPath)) { + New-Item -ItemType Directory -Force -Path $mirrorRootPath | Out-Null + } + $mirrorPath = Join-Path $mirrorRootPath "$($report.MachineName).json" + Copy-Item -Path $outputFullPath -Destination $mirrorPath -Force + Write-Host "Mirrored report: $mirrorPath" +} + +Write-Host "" +Write-Host "OfficeCom Sentinel runner summary" +Write-Host "Machine: $($report.MachineName)" +Write-Host "Events: $($report.TotalEvents)" +Write-Host "Unique IPs: $($report.UniqueIpCount)" +Write-Host "Status: $($report.AlertState)" +Write-Host "Reason: $($report.AlertReason)" +Write-Host "Base status: $($report.BaseAlertState)" +Write-Host "CVE findings: $($report.VulnerabilityCorrelation.TotalCount)" +Write-Host "Critical/High CVEs: $($report.VulnerabilityCorrelation.CriticalCount)" +Write-Host "Upload mode: $UploadMode" +Write-Host "Report: $outputFullPath" + +if ($report.Errors.Count -gt 0) { + Write-Host "Warnings:" + foreach ($warningEntry in $report.Errors) { + Write-Host "- $warningEntry" + } +} + +exit $exitCode diff --git a/installer/uninstall-attacktracer-ninja.ps1 b/installer/uninstall-attacktracer-ninja.ps1 index ab9667d..97b34c5 100644 --- a/installer/uninstall-attacktracer-ninja.ps1 +++ b/installer/uninstall-attacktracer-ninja.ps1 @@ -1,16 +1,15 @@ -param() +param( + [Parameter(ValueFromRemainingArguments = $true)] + [string[]]$RemainingArgs +) $ErrorActionPreference = "Stop" -$installRoot = Join-Path ${env:ProgramFiles} "OCSentinel" -$uninstallKey = "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\OCSentinel" - -if (Test-Path $uninstallKey) { - Remove-Item -Path $uninstallKey -Force -Recurse +$newScript = Join-Path $PSScriptRoot "uninstall-ocsentinel.ps1" +if (-not (Test-Path $newScript)) { + throw "Replacement script not found: $newScript" } -if (Test-Path $installRoot) { - Remove-Item -LiteralPath $installRoot -Force -Recurse -} - -Write-Host "OfficeCom Sentinel removed from $installRoot" +Write-Host "Compatibility wrapper: uninstall-attacktracer-ninja.ps1 -> uninstall-ocsentinel.ps1" +& powershell.exe -ExecutionPolicy Bypass -File $newScript @RemainingArgs +exit $LASTEXITCODE diff --git a/installer/uninstall-ocsentinel.ps1 b/installer/uninstall-ocsentinel.ps1 new file mode 100644 index 0000000..ab9667d --- /dev/null +++ b/installer/uninstall-ocsentinel.ps1 @@ -0,0 +1,16 @@ +param() + +$ErrorActionPreference = "Stop" + +$installRoot = Join-Path ${env:ProgramFiles} "OCSentinel" +$uninstallKey = "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\OCSentinel" + +if (Test-Path $uninstallKey) { + Remove-Item -Path $uninstallKey -Force -Recurse +} + +if (Test-Path $installRoot) { + Remove-Item -LiteralPath $installRoot -Force -Recurse +} + +Write-Host "OfficeCom Sentinel removed from $installRoot" diff --git a/installer/update-attacktracer-ninja.ps1 b/installer/update-attacktracer-ninja.ps1 index 30573c3..4c3fd89 100644 --- a/installer/update-attacktracer-ninja.ps1 +++ b/installer/update-attacktracer-ninja.ps1 @@ -1,131 +1,15 @@ param( - [string]$ManifestUrl = "", - [string]$Channel = "stable", - [string]$TempRoot = "$env:TEMP\OCSentinelUpdate", - [switch]$Force + [Parameter(ValueFromRemainingArguments = $true)] + [string[]]$RemainingArgs ) $ErrorActionPreference = "Stop" -$installRoot = Join-Path ${env:ProgramFiles} "OCSentinel" -$appExe = Join-Path $installRoot "app\OCSentinelCli.exe" -$installScript = Join-Path $installRoot "scripts\install-ocsentinel.ps1" -$versionFile = Join-Path $installRoot "VERSION.txt" - -function Get-InstalledVersion { - if (Test-Path $versionFile) { - return (Get-Content $versionFile -Raw).Trim() - } - - if (Test-Path $appExe) { - return (Get-Item $appExe).VersionInfo.ProductVersion - } - - return "0.0.0" +$newScript = Join-Path $PSScriptRoot "update-ocsentinel.ps1" +if (-not (Test-Path $newScript)) { + throw "Replacement script not found: $newScript" } -function Compare-Version { - param( - [Parameter(Mandatory)][string]$Left, - [Parameter(Mandatory)][string]$Right - ) - - try { - $leftVersion = [System.Version]$Left - $rightVersion = [System.Version]$Right - return $leftVersion.CompareTo($rightVersion) - } - catch { - return [string]::Compare($Left, $Right, $true) - } -} - -function Get-Sha256Hex { - param([Parameter(Mandatory)][string]$Path) - - return (Get-FileHash -Path $Path -Algorithm SHA256).Hash.ToLowerInvariant() -} - -function Resolve-ManifestUrl { - param( - [Parameter(Mandatory)][string]$ManifestUrl, - [Parameter(Mandatory)][string]$Channel - ) - - if ($ManifestUrl -match '\.json($|\?)') { - return $ManifestUrl - } - - return ($ManifestUrl.TrimEnd('/') + "/$Channel/version.json") -} - -if ([string]::IsNullOrWhiteSpace($ManifestUrl)) { - throw "ManifestUrl is required." -} - -$resolvedManifestUrl = Resolve-ManifestUrl -ManifestUrl $ManifestUrl -Channel $Channel -Write-Host "Checking update manifest: $resolvedManifestUrl" - -$manifest = Invoke-RestMethod -Method Get -Uri $resolvedManifestUrl -TimeoutSec 60 -if (-not $manifest.version -or -not $manifest.artifactUrl -or -not $manifest.sha256) { - throw "Update manifest is missing required fields: version, artifactUrl, sha256." -} - -$installedVersion = Get-InstalledVersion -$availableVersion = [string]$manifest.version - -Write-Host "Installed version: $installedVersion" -Write-Host "Available version: $availableVersion" - -if (-not $Force -and (Compare-Version -Left $installedVersion -Right $availableVersion) -ge 0) { - Write-Host "OfficeCom Sentinel is already up to date." - exit 0 -} - -$downloadRoot = Join-Path $TempRoot ([Guid]::NewGuid().ToString("N")) -$zipPath = Join-Path $downloadRoot "OCSentinelClient.zip" -$extractRoot = Join-Path $downloadRoot "payload" - -New-Item -ItemType Directory -Force -Path $downloadRoot, $extractRoot | Out-Null - -Write-Host "Downloading artifact: $($manifest.artifactUrl)" -Invoke-WebRequest -Uri ([string]$manifest.artifactUrl) -OutFile $zipPath -TimeoutSec 300 - -$actualHash = Get-Sha256Hex -Path $zipPath -$expectedHash = ([string]$manifest.sha256).ToLowerInvariant() -if ($actualHash -ne $expectedHash) { - throw "SHA-256 mismatch for downloaded artifact. Expected $expectedHash but got $actualHash." -} - -Write-Host "Artifact hash verified" -Expand-Archive -Path $zipPath -DestinationPath $extractRoot -Force - -$payloadAppExe = Get-ChildItem -Path $extractRoot -Recurse -Filter "OCSentinelCli.exe" | Select-Object -First 1 -if ($null -eq $payloadAppExe) { - throw "Downloaded payload did not contain OCSentinelCli.exe" -} - -$signature = Get-AuthenticodeSignature -FilePath $payloadAppExe.FullName -if ($signature.Status -notin @("Valid", "NotSigned")) { - throw "Executable signature validation failed with status: $($signature.Status)" -} - -if ($signature.Status -eq "NotSigned") { - Write-Warning "Downloaded executable is not code-signed yet. Hash validation succeeded, but code signing should be added before production rollout." -} -else { - Write-Host "Executable signature verified: $($signature.SignerCertificate.Subject)" -} - -$payloadInstallScript = Get-ChildItem -Path $extractRoot -Recurse -Filter "install-ocsentinel.ps1" | Select-Object -First 1 -if ($null -eq $payloadInstallScript) { - throw "Downloaded payload did not contain install-ocsentinel.ps1" -} - -Write-Host "Installing OfficeCom Sentinel $availableVersion" -& powershell.exe -ExecutionPolicy Bypass -File $payloadInstallScript.FullName -if ($LASTEXITCODE -ne 0) { - throw "Installer exited with code $LASTEXITCODE" -} - -Write-Host "Update complete: $installedVersion -> $availableVersion" +Write-Host "Compatibility wrapper: update-attacktracer-ninja.ps1 -> update-ocsentinel.ps1" +& powershell.exe -ExecutionPolicy Bypass -File $newScript @RemainingArgs +exit $LASTEXITCODE diff --git a/installer/update-ocsentinel.ps1 b/installer/update-ocsentinel.ps1 new file mode 100644 index 0000000..30573c3 --- /dev/null +++ b/installer/update-ocsentinel.ps1 @@ -0,0 +1,131 @@ +param( + [string]$ManifestUrl = "", + [string]$Channel = "stable", + [string]$TempRoot = "$env:TEMP\OCSentinelUpdate", + [switch]$Force +) + +$ErrorActionPreference = "Stop" + +$installRoot = Join-Path ${env:ProgramFiles} "OCSentinel" +$appExe = Join-Path $installRoot "app\OCSentinelCli.exe" +$installScript = Join-Path $installRoot "scripts\install-ocsentinel.ps1" +$versionFile = Join-Path $installRoot "VERSION.txt" + +function Get-InstalledVersion { + if (Test-Path $versionFile) { + return (Get-Content $versionFile -Raw).Trim() + } + + if (Test-Path $appExe) { + return (Get-Item $appExe).VersionInfo.ProductVersion + } + + return "0.0.0" +} + +function Compare-Version { + param( + [Parameter(Mandatory)][string]$Left, + [Parameter(Mandatory)][string]$Right + ) + + try { + $leftVersion = [System.Version]$Left + $rightVersion = [System.Version]$Right + return $leftVersion.CompareTo($rightVersion) + } + catch { + return [string]::Compare($Left, $Right, $true) + } +} + +function Get-Sha256Hex { + param([Parameter(Mandatory)][string]$Path) + + return (Get-FileHash -Path $Path -Algorithm SHA256).Hash.ToLowerInvariant() +} + +function Resolve-ManifestUrl { + param( + [Parameter(Mandatory)][string]$ManifestUrl, + [Parameter(Mandatory)][string]$Channel + ) + + if ($ManifestUrl -match '\.json($|\?)') { + return $ManifestUrl + } + + return ($ManifestUrl.TrimEnd('/') + "/$Channel/version.json") +} + +if ([string]::IsNullOrWhiteSpace($ManifestUrl)) { + throw "ManifestUrl is required." +} + +$resolvedManifestUrl = Resolve-ManifestUrl -ManifestUrl $ManifestUrl -Channel $Channel +Write-Host "Checking update manifest: $resolvedManifestUrl" + +$manifest = Invoke-RestMethod -Method Get -Uri $resolvedManifestUrl -TimeoutSec 60 +if (-not $manifest.version -or -not $manifest.artifactUrl -or -not $manifest.sha256) { + throw "Update manifest is missing required fields: version, artifactUrl, sha256." +} + +$installedVersion = Get-InstalledVersion +$availableVersion = [string]$manifest.version + +Write-Host "Installed version: $installedVersion" +Write-Host "Available version: $availableVersion" + +if (-not $Force -and (Compare-Version -Left $installedVersion -Right $availableVersion) -ge 0) { + Write-Host "OfficeCom Sentinel is already up to date." + exit 0 +} + +$downloadRoot = Join-Path $TempRoot ([Guid]::NewGuid().ToString("N")) +$zipPath = Join-Path $downloadRoot "OCSentinelClient.zip" +$extractRoot = Join-Path $downloadRoot "payload" + +New-Item -ItemType Directory -Force -Path $downloadRoot, $extractRoot | Out-Null + +Write-Host "Downloading artifact: $($manifest.artifactUrl)" +Invoke-WebRequest -Uri ([string]$manifest.artifactUrl) -OutFile $zipPath -TimeoutSec 300 + +$actualHash = Get-Sha256Hex -Path $zipPath +$expectedHash = ([string]$manifest.sha256).ToLowerInvariant() +if ($actualHash -ne $expectedHash) { + throw "SHA-256 mismatch for downloaded artifact. Expected $expectedHash but got $actualHash." +} + +Write-Host "Artifact hash verified" +Expand-Archive -Path $zipPath -DestinationPath $extractRoot -Force + +$payloadAppExe = Get-ChildItem -Path $extractRoot -Recurse -Filter "OCSentinelCli.exe" | Select-Object -First 1 +if ($null -eq $payloadAppExe) { + throw "Downloaded payload did not contain OCSentinelCli.exe" +} + +$signature = Get-AuthenticodeSignature -FilePath $payloadAppExe.FullName +if ($signature.Status -notin @("Valid", "NotSigned")) { + throw "Executable signature validation failed with status: $($signature.Status)" +} + +if ($signature.Status -eq "NotSigned") { + Write-Warning "Downloaded executable is not code-signed yet. Hash validation succeeded, but code signing should be added before production rollout." +} +else { + Write-Host "Executable signature verified: $($signature.SignerCertificate.Subject)" +} + +$payloadInstallScript = Get-ChildItem -Path $extractRoot -Recurse -Filter "install-ocsentinel.ps1" | Select-Object -First 1 +if ($null -eq $payloadInstallScript) { + throw "Downloaded payload did not contain install-ocsentinel.ps1" +} + +Write-Host "Installing OfficeCom Sentinel $availableVersion" +& powershell.exe -ExecutionPolicy Bypass -File $payloadInstallScript.FullName +if ($LASTEXITCODE -ne 0) { + throw "Installer exited with code $LASTEXITCODE" +} + +Write-Host "Update complete: $installedVersion -> $availableVersion" diff --git a/scripts/protect-attacktracer-secret.ps1 b/scripts/protect-attacktracer-secret.ps1 index edc56d2..5a1756d 100644 --- a/scripts/protect-attacktracer-secret.ps1 +++ b/scripts/protect-attacktracer-secret.ps1 @@ -1,25 +1,15 @@ param( - [Parameter(Mandatory)] - [string]$SecretValue, - [string]$OutputPath = "C:\ProgramData\OCSentinel\secrets\ocsentinel-upload-secret.dat" + [Parameter(ValueFromRemainingArguments = $true)] + [string[]]$RemainingArgs ) $ErrorActionPreference = "Stop" -Add-Type -AssemblyName System.Security - -$outputFullPath = [System.IO.Path]::GetFullPath($OutputPath) -$outputDirectory = Split-Path -Parent $outputFullPath -if (-not [string]::IsNullOrWhiteSpace($outputDirectory)) { - New-Item -ItemType Directory -Force -Path $outputDirectory | Out-Null +$newScript = Join-Path $PSScriptRoot "protect-ocsentinel-secret.ps1" +if (-not (Test-Path $newScript)) { + throw "Replacement script not found: $newScript" } -$secretBytes = [System.Text.Encoding]::UTF8.GetBytes($SecretValue) -$protectedBytes = [System.Security.Cryptography.ProtectedData]::Protect( - $secretBytes, - $null, - [System.Security.Cryptography.DataProtectionScope]::LocalMachine -) - -[System.IO.File]::WriteAllBytes($outputFullPath, $protectedBytes) -Write-Host "Protected secret written to $outputFullPath" +Write-Host "Compatibility wrapper: protect-attacktracer-secret.ps1 -> protect-ocsentinel-secret.ps1" +& powershell.exe -ExecutionPolicy Bypass -File $newScript @RemainingArgs +exit $LASTEXITCODE diff --git a/scripts/protect-ocsentinel-secret.ps1 b/scripts/protect-ocsentinel-secret.ps1 new file mode 100644 index 0000000..edc56d2 --- /dev/null +++ b/scripts/protect-ocsentinel-secret.ps1 @@ -0,0 +1,25 @@ +param( + [Parameter(Mandatory)] + [string]$SecretValue, + [string]$OutputPath = "C:\ProgramData\OCSentinel\secrets\ocsentinel-upload-secret.dat" +) + +$ErrorActionPreference = "Stop" + +Add-Type -AssemblyName System.Security + +$outputFullPath = [System.IO.Path]::GetFullPath($OutputPath) +$outputDirectory = Split-Path -Parent $outputFullPath +if (-not [string]::IsNullOrWhiteSpace($outputDirectory)) { + New-Item -ItemType Directory -Force -Path $outputDirectory | Out-Null +} + +$secretBytes = [System.Text.Encoding]::UTF8.GetBytes($SecretValue) +$protectedBytes = [System.Security.Cryptography.ProtectedData]::Protect( + $secretBytes, + $null, + [System.Security.Cryptography.DataProtectionScope]::LocalMachine +) + +[System.IO.File]::WriteAllBytes($outputFullPath, $protectedBytes) +Write-Host "Protected secret written to $outputFullPath" diff --git a/scripts/run-attacktracer-ninja-monitor.ps1 b/scripts/run-attacktracer-ninja-monitor.ps1 index 778db62..ef468c7 100644 --- a/scripts/run-attacktracer-ninja-monitor.ps1 +++ b/scripts/run-attacktracer-ninja-monitor.ps1 @@ -1,213 +1,15 @@ param( - [int]$LookbackDays = 7, - [int]$TopCount = 10, - [string]$OutputPath = ".\reports\attacktracer-summary.json", - [string]$ConfigPath = ".\config\attacktracer-settings.example.json", - [string]$VulnerabilityCsvPath = "", - [string]$MirrorRoot = "", - [ValidateSet("status", "attack-only", "cve-critical", "attack-plus-cve")] - [string]$Mode = "status" + [Parameter(ValueFromRemainingArguments = $true)] + [string[]]$RemainingArgs ) $ErrorActionPreference = "Stop" -$repoRoot = Split-Path -Parent $PSScriptRoot -$runnerScript = Join-Path $repoRoot "scripts\run-attacktracer-ninja.ps1" -$outputFullPath = [System.IO.Path]::GetFullPath((Join-Path $repoRoot $OutputPath)) - -$script:NinjaFieldBackend = $null -$script:NinjaCliPath = "C:\ProgramData\NinjaRMMAgent\ninjarmm-cli.exe" - -function Resolve-PathLike { - param( - [Parameter(Mandatory)] - [string]$PathValue, - [Parameter(Mandatory)] - [string]$BasePath - ) - - if ([string]::IsNullOrWhiteSpace($PathValue)) { - return $PathValue - } - - if ([System.IO.Path]::IsPathRooted($PathValue) -or $PathValue.StartsWith("\\")) { - return [System.IO.Path]::GetFullPath($PathValue) - } - - return [System.IO.Path]::GetFullPath((Join-Path $BasePath $PathValue)) +$newScript = Join-Path $PSScriptRoot "run-ocsentinel-monitor.ps1" +if (-not (Test-Path $newScript)) { + throw "Replacement script not found: $newScript" } -function Initialize-NinjaFieldWriter { - if ($null -ne $script:NinjaFieldBackend) { - return - } - - if (Get-Command -Name "Ninja-Property-Set" -ErrorAction SilentlyContinue) { - $script:NinjaFieldBackend = "powershell" - return - } - - if (Test-Path $script:NinjaCliPath) { - $script:NinjaFieldBackend = "cli" - return - } - - $script:NinjaFieldBackend = "none" -} - -function Set-NinjaCustomFieldValue { - param( - [Parameter(Mandatory)] - [string]$Name, - [AllowEmptyString()] - [string]$Value - ) - - Initialize-NinjaFieldWriter - - switch ($script:NinjaFieldBackend) { - "powershell" { - Ninja-Property-Set $Name $Value | Out-Null - return $true - } - "cli" { - & $script:NinjaCliPath set $Name $Value | Out-Null - return $LASTEXITCODE -eq 0 - } - default { - return $false - } - } -} - -function Publish-NinjaCustomFields { - param( - [Parameter(Mandatory)] - [pscustomobject]$Report, - [Parameter(Mandatory)] - [string]$Mode, - [Parameter(Mandatory)] - [bool]$Triggered, - [Parameter(Mandatory)] - [string]$Reason - ) - - Initialize-NinjaFieldWriter - if ($script:NinjaFieldBackend -eq "none") { - Write-Host "Ninja custom fields: skipped (Ninja field writer not available)." - return - } - - $generatedAtUtc = "" - if ($Report.GeneratedAtLocal) { - try { - $generatedAtUtc = ([DateTimeOffset]$Report.GeneratedAtLocal).ToUniversalTime().ToString("o") - } - catch { - $generatedAtUtc = [string]$Report.GeneratedAtLocal - } - } - - $fieldValues = [ordered]@{ - "attacktracerstatus" = [string]$Report.AlertState - "attacktracerreason" = $Reason - "attacktracerbasestatus" = [string]$Report.BaseAlertState - "attacktracerevents" = [string]([int]$Report.TotalEvents) - "attacktraceruniqueips" = [string]([int]$Report.UniqueIpCount) - "attacktracercvecritical" = [string]([int]$Report.VulnerabilityCorrelation.CriticalCount) - "attacktracercvetotal" = [string]([int]$Report.VulnerabilityCorrelation.TotalCount) - "attacktracermode" = $Mode - "attacktracertriggered" = $Triggered.ToString().ToLowerInvariant() - "attacktracerlastscanutc" = $generatedAtUtc - } - - $updated = 0 - foreach ($entry in $fieldValues.GetEnumerator()) { - try { - if (Set-NinjaCustomFieldValue -Name $entry.Key -Value $entry.Value) { - $updated++ - } - } - catch { - Write-Warning "Failed to set Ninja custom field '$($entry.Key)': $($_.Exception.Message)" - } - } - - Write-Host "Ninja custom fields: updated $updated field(s) via $script:NinjaFieldBackend." -} - -$runnerArgs = @( - "-ExecutionPolicy", "Bypass", - "-File", $runnerScript, - "-LookbackDays", $LookbackDays, - "-TopCount", $TopCount, - "-OutputPath", $OutputPath, - "-ConfigPath", $ConfigPath -) - -if (-not [string]::IsNullOrWhiteSpace($VulnerabilityCsvPath)) { - $runnerArgs += @("-VulnerabilityCsvPath", $VulnerabilityCsvPath) -} - -if (-not [string]::IsNullOrWhiteSpace($MirrorRoot)) { - $runnerArgs += @("-MirrorRoot", (Resolve-PathLike -PathValue $MirrorRoot -BasePath $repoRoot)) -} - -$null = & powershell @runnerArgs -$runnerExitCode = $LASTEXITCODE - -if (-not (Test-Path $outputFullPath)) { - throw "Expected report file was not created: $outputFullPath" -} - -$report = Get-Content $outputFullPath -Raw | ConvertFrom-Json - -$status = [string]$report.AlertState -$baseStatus = [string]$report.BaseAlertState -$events = [int]$report.TotalEvents -$uniqueIps = [int]$report.UniqueIpCount -$criticalCves = [int]$report.VulnerabilityCorrelation.CriticalCount -$totalCves = [int]$report.VulnerabilityCorrelation.TotalCount - -$monitorTriggered = $false -$monitorReason = "" - -switch ($Mode) { - "status" { - $monitorTriggered = $status -ne "ok" - $monitorReason = "Final status is $status. $($report.AlertReason)" - } - "attack-only" { - $monitorTriggered = $baseStatus -ne "ok" - $monitorReason = "Base attack status is $baseStatus. $($report.BaseAlertReason)" - } - "cve-critical" { - $monitorTriggered = $criticalCves -gt 0 - $monitorReason = "Critical/high CVE count is $criticalCves out of total CVEs $totalCves." - } - "attack-plus-cve" { - $monitorTriggered = ($events -gt 0 -and $criticalCves -gt 0) - $monitorReason = "Attack events=$events and critical/high CVEs=$criticalCves." - } -} - -Publish-NinjaCustomFields -Report $report -Mode $Mode -Triggered $monitorTriggered -Reason $monitorReason - -Write-Host "" -Write-Host "AttackTracer Ninja monitor mode: $Mode" -Write-Host "Triggered: $monitorTriggered" -Write-Host "Reason: $monitorReason" -Write-Host "Status: $status" -Write-Host "Base status: $baseStatus" -Write-Host "Events: $events" -Write-Host "Unique IPs: $uniqueIps" -Write-Host "Critical/High CVEs: $criticalCves" -Write-Host "Total CVEs: $totalCves" -Write-Host "Report: $outputFullPath" -Write-Host "Runner exit code: $runnerExitCode" - -if ($monitorTriggered) { - exit 1 -} - -exit 0 +Write-Host "Compatibility wrapper: run-attacktracer-ninja-monitor.ps1 -> run-ocsentinel-monitor.ps1" +& powershell.exe -ExecutionPolicy Bypass -File $newScript @RemainingArgs +exit $LASTEXITCODE diff --git a/scripts/run-attacktracer-ninja.ps1 b/scripts/run-attacktracer-ninja.ps1 index 0e33d84..a7f04db 100644 --- a/scripts/run-attacktracer-ninja.ps1 +++ b/scripts/run-attacktracer-ninja.ps1 @@ -1,144 +1,15 @@ param( - [int]$LookbackDays = 7, - [int]$TopCount = 10, - [string]$OutputPath = ".\reports\attacktracer-summary.json", - [string]$ConfigPath = ".\config\attacktracer-settings.example.json", - [string]$ClientConfigPath = ".\config\attacktracer-client.example.json", - [string]$SecretPath = "", - [string]$VulnerabilityCsvPath = "", - [string]$MirrorRoot = "", - [ValidateSet("disabled", "auto", "required")] - [string]$UploadMode = "auto", - [switch]$FailOnAttacks, - [switch]$FailOnThreshold + [Parameter(ValueFromRemainingArguments = $true)] + [string[]]$RemainingArgs ) $ErrorActionPreference = "Stop" -$repoRoot = Split-Path -Parent $PSScriptRoot -$projectPath = Join-Path $repoRoot "src\AttackTracerNinjaCli\AttackTracerNinjaCli.csproj" -$outputFullPath = [System.IO.Path]::GetFullPath((Join-Path $repoRoot $OutputPath)) -$buildOutputDir = Join-Path $repoRoot "src\AttackTracerNinjaCli\bin\Debug\net10.0" -$dllPath = Join-Path $buildOutputDir "AttackTracerNinjaCli.dll" - -function Resolve-PathLike { - param( - [Parameter(Mandatory)] - [string]$PathValue, - [Parameter(Mandatory)] - [string]$BasePath - ) - - if ([string]::IsNullOrWhiteSpace($PathValue)) { - return $PathValue - } - - if ([System.IO.Path]::IsPathRooted($PathValue) -or $PathValue.StartsWith("\\")) { - return [System.IO.Path]::GetFullPath($PathValue) - } - - return [System.IO.Path]::GetFullPath((Join-Path $BasePath $PathValue)) +$newScript = Join-Path $PSScriptRoot "run-ocsentinel.ps1" +if (-not (Test-Path $newScript)) { + throw "Replacement script not found: $newScript" } -$arguments = @( - $dllPath -) - -$configFullPath = [System.IO.Path]::GetFullPath((Join-Path $repoRoot $ConfigPath)) -if ($UploadMode -eq "disabled") { - $arguments += "scan" -} -else { - $clientConfigFullPath = Resolve-PathLike -PathValue $ClientConfigPath -BasePath $repoRoot - $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) - - if ($UploadMode -eq "required" -and -not $canUpload) { - throw "UploadMode 'required' was set, but client config or protected secret is missing." - } - - if ($canUpload) { - $arguments += "scan-and-upload" - $arguments += @("--client-config", $clientConfigFullPath, "--secret-path", $secretFullPath) - } - else { - $arguments += "scan" - } -} - -$arguments += @( - "--lookback-days", $LookbackDays, - "--top", $TopCount, - "--output", $outputFullPath, - "--ninja-output" -) - -if (Test-Path $configFullPath) { - $arguments += @("--config", $configFullPath) -} - -if (-not [string]::IsNullOrWhiteSpace($VulnerabilityCsvPath)) { - $vulnerabilityCsvFullPath = [System.IO.Path]::GetFullPath((Join-Path $repoRoot $VulnerabilityCsvPath)) - if (Test-Path $vulnerabilityCsvFullPath) { - $arguments += @("--vulnerability-csv", $vulnerabilityCsvFullPath) - } -} - -if ($FailOnAttacks) { - $arguments += "--fail-on-attacks" -} - -if ($FailOnThreshold) { - $arguments += "--fail-on-threshold" -} - -$null = & dotnet build $projectPath -if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE -} - -if (-not (Test-Path $dllPath)) { - throw "Expected compiled DLL was not created: $dllPath" -} - -& dotnet @arguments -$exitCode = $LASTEXITCODE - -if (-not (Test-Path $outputFullPath)) { - throw "Expected report file was not created: $outputFullPath" -} - -$report = Get-Content $outputFullPath -Raw | ConvertFrom-Json - -if (-not [string]::IsNullOrWhiteSpace($MirrorRoot)) { - Write-Host "Legacy mirror mode enabled." - $mirrorRootPath = Resolve-PathLike -PathValue $MirrorRoot -BasePath $repoRoot - if (-not (Test-Path $mirrorRootPath)) { - New-Item -ItemType Directory -Force -Path $mirrorRootPath | Out-Null - } - $mirrorPath = Join-Path $mirrorRootPath "$($report.MachineName).json" - Copy-Item -Path $outputFullPath -Destination $mirrorPath -Force - Write-Host "Mirrored report: $mirrorPath" -} - -Write-Host "" -Write-Host "Ninja wrapper summary" -Write-Host "Machine: $($report.MachineName)" -Write-Host "Events: $($report.TotalEvents)" -Write-Host "Unique IPs: $($report.UniqueIpCount)" -Write-Host "Status: $($report.AlertState)" -Write-Host "Reason: $($report.AlertReason)" -Write-Host "Base status: $($report.BaseAlertState)" -Write-Host "CVE findings: $($report.VulnerabilityCorrelation.TotalCount)" -Write-Host "Critical/High CVEs: $($report.VulnerabilityCorrelation.CriticalCount)" -Write-Host "Upload mode: $UploadMode" -Write-Host "Report: $outputFullPath" - -if ($report.Errors.Count -gt 0) { - Write-Host "Warnings:" - foreach ($warningEntry in $report.Errors) { - Write-Host "- $warningEntry" - } -} - -exit $exitCode +Write-Host "Compatibility wrapper: run-attacktracer-ninja.ps1 -> run-ocsentinel.ps1" +& powershell.exe -ExecutionPolicy Bypass -File $newScript @RemainingArgs +exit $LASTEXITCODE diff --git a/scripts/run-ocsentinel-monitor.ps1 b/scripts/run-ocsentinel-monitor.ps1 new file mode 100644 index 0000000..e88ce1d --- /dev/null +++ b/scripts/run-ocsentinel-monitor.ps1 @@ -0,0 +1,213 @@ +param( + [int]$LookbackDays = 7, + [int]$TopCount = 10, + [string]$OutputPath = ".\reports\ocsentinel-summary.json", + [string]$ConfigPath = ".\config\ocsentinel-settings.example.json", + [string]$VulnerabilityCsvPath = "", + [string]$MirrorRoot = "", + [ValidateSet("status", "attack-only", "cve-critical", "attack-plus-cve")] + [string]$Mode = "status" +) + +$ErrorActionPreference = "Stop" + +$repoRoot = Split-Path -Parent $PSScriptRoot +$runnerScript = Join-Path $repoRoot "scripts\run-ocsentinel.ps1" +$outputFullPath = [System.IO.Path]::GetFullPath((Join-Path $repoRoot $OutputPath)) + +$script:NinjaFieldBackend = $null +$script:NinjaCliPath = "C:\ProgramData\NinjaRMMAgent\ninjarmm-cli.exe" + +function Resolve-PathLike { + param( + [Parameter(Mandatory)] + [string]$PathValue, + [Parameter(Mandatory)] + [string]$BasePath + ) + + if ([string]::IsNullOrWhiteSpace($PathValue)) { + return $PathValue + } + + if ([System.IO.Path]::IsPathRooted($PathValue) -or $PathValue.StartsWith("\\")) { + return [System.IO.Path]::GetFullPath($PathValue) + } + + return [System.IO.Path]::GetFullPath((Join-Path $BasePath $PathValue)) +} + +function Initialize-NinjaFieldWriter { + if ($null -ne $script:NinjaFieldBackend) { + return + } + + if (Get-Command -Name "Ninja-Property-Set" -ErrorAction SilentlyContinue) { + $script:NinjaFieldBackend = "powershell" + return + } + + if (Test-Path $script:NinjaCliPath) { + $script:NinjaFieldBackend = "cli" + return + } + + $script:NinjaFieldBackend = "none" +} + +function Set-NinjaCustomFieldValue { + param( + [Parameter(Mandatory)] + [string]$Name, + [AllowEmptyString()] + [string]$Value + ) + + Initialize-NinjaFieldWriter + + switch ($script:NinjaFieldBackend) { + "powershell" { + Ninja-Property-Set $Name $Value | Out-Null + return $true + } + "cli" { + & $script:NinjaCliPath set $Name $Value | Out-Null + return $LASTEXITCODE -eq 0 + } + default { + return $false + } + } +} + +function Publish-NinjaCustomFields { + param( + [Parameter(Mandatory)] + [pscustomobject]$Report, + [Parameter(Mandatory)] + [string]$Mode, + [Parameter(Mandatory)] + [bool]$Triggered, + [Parameter(Mandatory)] + [string]$Reason + ) + + Initialize-NinjaFieldWriter + if ($script:NinjaFieldBackend -eq "none") { + Write-Host "Ninja custom fields: skipped (Ninja field writer not available)." + return + } + + $generatedAtUtc = "" + if ($Report.GeneratedAtLocal) { + try { + $generatedAtUtc = ([DateTimeOffset]$Report.GeneratedAtLocal).ToUniversalTime().ToString("o") + } + catch { + $generatedAtUtc = [string]$Report.GeneratedAtLocal + } + } + + $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 + } + + $updated = 0 + foreach ($entry in $fieldValues.GetEnumerator()) { + try { + if (Set-NinjaCustomFieldValue -Name $entry.Key -Value $entry.Value) { + $updated++ + } + } + catch { + Write-Warning "Failed to set Ninja custom field '$($entry.Key)': $($_.Exception.Message)" + } + } + + Write-Host "Ninja custom fields: updated $updated field(s) via $script:NinjaFieldBackend." +} + +$runnerArgs = @( + "-ExecutionPolicy", "Bypass", + "-File", $runnerScript, + "-LookbackDays", $LookbackDays, + "-TopCount", $TopCount, + "-OutputPath", $OutputPath, + "-ConfigPath", $ConfigPath +) + +if (-not [string]::IsNullOrWhiteSpace($VulnerabilityCsvPath)) { + $runnerArgs += @("-VulnerabilityCsvPath", $VulnerabilityCsvPath) +} + +if (-not [string]::IsNullOrWhiteSpace($MirrorRoot)) { + $runnerArgs += @("-MirrorRoot", (Resolve-PathLike -PathValue $MirrorRoot -BasePath $repoRoot)) +} + +$null = & powershell @runnerArgs +$runnerExitCode = $LASTEXITCODE + +if (-not (Test-Path $outputFullPath)) { + throw "Expected report file was not created: $outputFullPath" +} + +$report = Get-Content $outputFullPath -Raw | ConvertFrom-Json + +$status = [string]$report.AlertState +$baseStatus = [string]$report.BaseAlertState +$events = [int]$report.TotalEvents +$uniqueIps = [int]$report.UniqueIpCount +$criticalCves = [int]$report.VulnerabilityCorrelation.CriticalCount +$totalCves = [int]$report.VulnerabilityCorrelation.TotalCount + +$monitorTriggered = $false +$monitorReason = "" + +switch ($Mode) { + "status" { + $monitorTriggered = $status -ne "ok" + $monitorReason = "Final status is $status. $($report.AlertReason)" + } + "attack-only" { + $monitorTriggered = $baseStatus -ne "ok" + $monitorReason = "Base attack status is $baseStatus. $($report.BaseAlertReason)" + } + "cve-critical" { + $monitorTriggered = $criticalCves -gt 0 + $monitorReason = "Critical/high CVE count is $criticalCves out of total CVEs $totalCves." + } + "attack-plus-cve" { + $monitorTriggered = ($events -gt 0 -and $criticalCves -gt 0) + $monitorReason = "Attack events=$events and critical/high CVEs=$criticalCves." + } +} + +Publish-NinjaCustomFields -Report $report -Mode $Mode -Triggered $monitorTriggered -Reason $monitorReason + +Write-Host "" +Write-Host "OfficeCom Sentinel monitor mode: $Mode" +Write-Host "Triggered: $monitorTriggered" +Write-Host "Reason: $monitorReason" +Write-Host "Status: $status" +Write-Host "Base status: $baseStatus" +Write-Host "Events: $events" +Write-Host "Unique IPs: $uniqueIps" +Write-Host "Critical/High CVEs: $criticalCves" +Write-Host "Total CVEs: $totalCves" +Write-Host "Report: $outputFullPath" +Write-Host "Runner exit code: $runnerExitCode" + +if ($monitorTriggered) { + exit 1 +} + +exit 0 diff --git a/scripts/run-ocsentinel.ps1 b/scripts/run-ocsentinel.ps1 new file mode 100644 index 0000000..4aa1e4f --- /dev/null +++ b/scripts/run-ocsentinel.ps1 @@ -0,0 +1,144 @@ +param( + [int]$LookbackDays = 7, + [int]$TopCount = 10, + [string]$OutputPath = ".\reports\ocsentinel-summary.json", + [string]$ConfigPath = ".\config\ocsentinel-settings.example.json", + [string]$ClientConfigPath = ".\config\ocsentinel-client.example.json", + [string]$SecretPath = "", + [string]$VulnerabilityCsvPath = "", + [string]$MirrorRoot = "", + [ValidateSet("disabled", "auto", "required")] + [string]$UploadMode = "auto", + [switch]$FailOnAttacks, + [switch]$FailOnThreshold +) + +$ErrorActionPreference = "Stop" + +$repoRoot = Split-Path -Parent $PSScriptRoot +$projectPath = Join-Path $repoRoot "src\AttackTracerNinjaCli\AttackTracerNinjaCli.csproj" +$outputFullPath = [System.IO.Path]::GetFullPath((Join-Path $repoRoot $OutputPath)) +$buildOutputDir = Join-Path $repoRoot "src\AttackTracerNinjaCli\bin\Debug\net10.0" +$dllPath = Join-Path $buildOutputDir "OCSentinelCli.dll" + +function Resolve-PathLike { + param( + [Parameter(Mandatory)] + [string]$PathValue, + [Parameter(Mandatory)] + [string]$BasePath + ) + + if ([string]::IsNullOrWhiteSpace($PathValue)) { + return $PathValue + } + + if ([System.IO.Path]::IsPathRooted($PathValue) -or $PathValue.StartsWith("\\")) { + return [System.IO.Path]::GetFullPath($PathValue) + } + + return [System.IO.Path]::GetFullPath((Join-Path $BasePath $PathValue)) +} + +$arguments = @( + $dllPath +) + +$configFullPath = [System.IO.Path]::GetFullPath((Join-Path $repoRoot $ConfigPath)) +if ($UploadMode -eq "disabled") { + $arguments += "scan" +} +else { + $clientConfigFullPath = Resolve-PathLike -PathValue $ClientConfigPath -BasePath $repoRoot + $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) + + if ($UploadMode -eq "required" -and -not $canUpload) { + throw "UploadMode 'required' was set, but client config or protected secret is missing." + } + + if ($canUpload) { + $arguments += "scan-and-upload" + $arguments += @("--client-config", $clientConfigFullPath, "--secret-path", $secretFullPath) + } + else { + $arguments += "scan" + } +} + +$arguments += @( + "--lookback-days", $LookbackDays, + "--top", $TopCount, + "--output", $outputFullPath, + "--ninja-output" +) + +if (Test-Path $configFullPath) { + $arguments += @("--config", $configFullPath) +} + +if (-not [string]::IsNullOrWhiteSpace($VulnerabilityCsvPath)) { + $vulnerabilityCsvFullPath = [System.IO.Path]::GetFullPath((Join-Path $repoRoot $VulnerabilityCsvPath)) + if (Test-Path $vulnerabilityCsvFullPath) { + $arguments += @("--vulnerability-csv", $vulnerabilityCsvFullPath) + } +} + +if ($FailOnAttacks) { + $arguments += "--fail-on-attacks" +} + +if ($FailOnThreshold) { + $arguments += "--fail-on-threshold" +} + +$null = & dotnet build $projectPath +if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE +} + +if (-not (Test-Path $dllPath)) { + throw "Expected compiled DLL was not created: $dllPath" +} + +& dotnet @arguments +$exitCode = $LASTEXITCODE + +if (-not (Test-Path $outputFullPath)) { + throw "Expected report file was not created: $outputFullPath" +} + +$report = Get-Content $outputFullPath -Raw | ConvertFrom-Json + +if (-not [string]::IsNullOrWhiteSpace($MirrorRoot)) { + Write-Host "Legacy mirror mode enabled." + $mirrorRootPath = Resolve-PathLike -PathValue $MirrorRoot -BasePath $repoRoot + if (-not (Test-Path $mirrorRootPath)) { + New-Item -ItemType Directory -Force -Path $mirrorRootPath | Out-Null + } + $mirrorPath = Join-Path $mirrorRootPath "$($report.MachineName).json" + Copy-Item -Path $outputFullPath -Destination $mirrorPath -Force + Write-Host "Mirrored report: $mirrorPath" +} + +Write-Host "" +Write-Host "OfficeCom Sentinel wrapper summary" +Write-Host "Machine: $($report.MachineName)" +Write-Host "Events: $($report.TotalEvents)" +Write-Host "Unique IPs: $($report.UniqueIpCount)" +Write-Host "Status: $($report.AlertState)" +Write-Host "Reason: $($report.AlertReason)" +Write-Host "Base status: $($report.BaseAlertState)" +Write-Host "CVE findings: $($report.VulnerabilityCorrelation.TotalCount)" +Write-Host "Critical/High CVEs: $($report.VulnerabilityCorrelation.CriticalCount)" +Write-Host "Upload mode: $UploadMode" +Write-Host "Report: $outputFullPath" + +if ($report.Errors.Count -gt 0) { + Write-Host "Warnings:" + foreach ($warningEntry in $report.Errors) { + Write-Host "- $warningEntry" + } +} + +exit $exitCode diff --git a/src/AttackTracerNinjaCli/Commands/ScanAndUploadCommand.cs b/src/AttackTracerNinjaCli/Commands/ScanAndUploadCommand.cs index f7149d2..9ff7d5f 100644 --- a/src/AttackTracerNinjaCli/Commands/ScanAndUploadCommand.cs +++ b/src/AttackTracerNinjaCli/Commands/ScanAndUploadCommand.cs @@ -4,7 +4,7 @@ internal static class ScanAndUploadCommand { public static int Execute(string[] args) { - string outputPath = @"C:\ProgramData\AttackTracerNinja\reports\latest.json"; + string outputPath = @"C:\ProgramData\OCSentinel\reports\latest.json"; bool hasOutput = false; for (int i = 0; i < args.Length; i++) diff --git a/src/AttackTracerNinjaCli/Commands/UploadCommand.cs b/src/AttackTracerNinjaCli/Commands/UploadCommand.cs index 86e3206..696e3ed 100644 --- a/src/AttackTracerNinjaCli/Commands/UploadCommand.cs +++ b/src/AttackTracerNinjaCli/Commands/UploadCommand.cs @@ -41,7 +41,7 @@ internal static class UploadCommand } string resolvedConfigPath = Path.GetFullPath(configPath ?? @"C:\ProgramData\OCSentinel\config\ocsentinel-client.json"); - string resolvedSecretPath = Path.GetFullPath(secretPath ?? @"C:\ProgramData\OCSentinel\secrets\upload-secret.dat"); + string resolvedSecretPath = Path.GetFullPath(secretPath ?? @"C:\ProgramData\OCSentinel\secrets\ocsentinel-upload-secret.dat"); ClientConfiguration config = ClientConfiguration.Load(resolvedConfigPath); if (string.IsNullOrWhiteSpace(config.N8nWebhookUrl))