Adopt OCSentinel script layout and add Gitea workflow
Some checks failed
OfficeCom Sentinel Client / build-client (push) Has been cancelled
Some checks failed
OfficeCom Sentinel Client / build-client (push) Has been cancelled
This commit is contained in:
46
.gitea/workflows/client-build.yml
Normal file
46
.gitea/workflows/client-build.yml
Normal file
@@ -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
|
||||||
38
README.md
Normal file
38
README.md
Normal file
@@ -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.
|
||||||
@@ -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 $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 "install-ocsentinel.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 "uninstall-ocsentinel.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 "update-ocsentinel.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-ocsentinel.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 $installerRoot "runtime-run-ocsentinel-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 $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-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
|
Copy-Item -Path (Join-Path $repoRoot "config\attacktracer-client.example.json") -Destination (Join-Path $packageRoot "config\ocsentinel-client.example.json") -Force
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
# AttackTracer Ninja V2 Deployment
|
# OfficeCom Sentinel Deployment
|
||||||
|
|
||||||
## Goal
|
## 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
|
## Release assets
|
||||||
|
|
||||||
Each GitLab release should publish:
|
Each Gitea release should publish:
|
||||||
|
|
||||||
- `OCSentinelClient-win-x64.zip`
|
- `OCSentinelClient-win-x64.zip`
|
||||||
- `OCSentinelClient-win-x64.zip.sha256`
|
- `OCSentinelClient-win-x64.zip.sha256`
|
||||||
@@ -17,7 +17,7 @@ Build these locally with:
|
|||||||
```powershell
|
```powershell
|
||||||
powershell -ExecutionPolicy Bypass -File .\build\build-client-package.ps1
|
powershell -ExecutionPolicy Bypass -File .\build\build-client-package.ps1
|
||||||
powershell -ExecutionPolicy Bypass -File .\build\build-release-manifest.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:
|
See example manifest:
|
||||||
@@ -42,7 +42,7 @@ Recommended NinjaOne task:
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
& "C:\Program Files\OCSentinel\scripts\update-ocsentinel.ps1" `
|
& "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
|
-Force
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ Recommended scheduled task command:
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
& "C:\Program Files\OCSentinel\scripts\update-ocsentinel.ps1" `
|
& "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
|
## Behavior
|
||||||
@@ -88,12 +88,12 @@ Recommended runtime task:
|
|||||||
```powershell
|
```powershell
|
||||||
& "C:\Program Files\OCSentinel\scripts\run-ocsentinel-monitor.ps1" `
|
& "C:\Program Files\OCSentinel\scripts\run-ocsentinel-monitor.ps1" `
|
||||||
-Mode status `
|
-Mode status `
|
||||||
-OutputPath "..\reports\attacktracer-summary.json"
|
-OutputPath "..\reports\ocsentinel-summary.json"
|
||||||
```
|
```
|
||||||
|
|
||||||
Upload is enabled automatically when:
|
Upload is enabled automatically when:
|
||||||
|
|
||||||
- `config\attacktracer-client.json` exists
|
- `config\ocsentinel-client.json` exists
|
||||||
- the protected secret file exists
|
- the protected secret file exists
|
||||||
|
|
||||||
Otherwise the client falls back to local scan behavior.
|
Otherwise the client falls back to local scan behavior.
|
||||||
|
|||||||
@@ -1,77 +1,15 @@
|
|||||||
param()
|
param(
|
||||||
|
[Parameter(ValueFromRemainingArguments = $true)]
|
||||||
|
[string[]]$RemainingArgs
|
||||||
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
$scriptPath = $MyInvocation.MyCommand.Path
|
$newScript = Join-Path $PSScriptRoot "install-ocsentinel.ps1"
|
||||||
$scriptDirectory = Split-Path -Parent $scriptPath
|
if (-not (Test-Path $newScript)) {
|
||||||
$packageRoot = if ((Split-Path -Leaf $scriptDirectory) -ieq "scripts") { Split-Path -Parent $scriptDirectory } else { $scriptDirectory }
|
throw "Replacement script not found: $newScript"
|
||||||
$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"
|
Write-Host "Compatibility wrapper: install-attacktracer-ninja.ps1 -> install-ocsentinel.ps1"
|
||||||
$clientExampleConfig = Join-Path $configRoot "ocsentinel-client.example.json"
|
& powershell.exe -ExecutionPolicy Bypass -File $newScript @RemainingArgs
|
||||||
if (-not (Test-Path $clientMainConfig) -and (Test-Path $clientExampleConfig)) {
|
exit $LASTEXITCODE
|
||||||
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')"
|
|
||||||
|
|||||||
77
installer/install-ocsentinel.ps1
Normal file
77
installer/install-ocsentinel.ps1
Normal file
@@ -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')"
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
@echo off
|
@echo off
|
||||||
powershell.exe -ExecutionPolicy Bypass -File "%~dp0install-attacktracer-ninja.ps1"
|
powershell.exe -ExecutionPolicy Bypass -File "%~dp0install-ocsentinel.ps1"
|
||||||
exit /b %errorlevel%
|
exit /b %errorlevel%
|
||||||
|
|||||||
@@ -1,213 +1,15 @@
|
|||||||
param(
|
param(
|
||||||
[int]$LookbackDays = 7,
|
[Parameter(ValueFromRemainingArguments = $true)]
|
||||||
[int]$TopCount = 10,
|
[string[]]$RemainingArgs
|
||||||
[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"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
$newScript = Join-Path $PSScriptRoot "runtime-run-ocsentinel-monitor.ps1"
|
||||||
$runnerScript = Join-Path $scriptDir "run-ocsentinel.ps1"
|
if (-not (Test-Path $newScript)) {
|
||||||
$outputFullPath = [System.IO.Path]::GetFullPath((Join-Path $scriptDir $OutputPath))
|
throw "Replacement script not found: $newScript"
|
||||||
|
|
||||||
$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 {
|
Write-Host "Compatibility wrapper: runtime-run-attacktracer-ninja-monitor.ps1 -> runtime-run-ocsentinel-monitor.ps1"
|
||||||
if ($null -ne $script:NinjaFieldBackend) {
|
& powershell.exe -ExecutionPolicy Bypass -File $newScript @RemainingArgs
|
||||||
return
|
exit $LASTEXITCODE
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|||||||
@@ -1,136 +1,15 @@
|
|||||||
param(
|
param(
|
||||||
[int]$LookbackDays = 7,
|
[Parameter(ValueFromRemainingArguments = $true)]
|
||||||
[int]$TopCount = 10,
|
[string[]]$RemainingArgs
|
||||||
[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"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
$newScript = Join-Path $PSScriptRoot "runtime-run-ocsentinel.ps1"
|
||||||
$installRoot = Split-Path -Parent $scriptDir
|
if (-not (Test-Path $newScript)) {
|
||||||
$appExe = Join-Path $installRoot "app\OCSentinelCli.exe"
|
throw "Replacement script not found: $newScript"
|
||||||
$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)) {
|
Write-Host "Compatibility wrapper: runtime-run-attacktracer-ninja.ps1 -> runtime-run-ocsentinel.ps1"
|
||||||
throw "Application executable not found: $appExe"
|
& powershell.exe -ExecutionPolicy Bypass -File $newScript @RemainingArgs
|
||||||
}
|
exit $LASTEXITCODE
|
||||||
|
|
||||||
$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
|
|
||||||
|
|||||||
213
installer/runtime-run-ocsentinel-monitor.ps1
Normal file
213
installer/runtime-run-ocsentinel-monitor.ps1
Normal file
@@ -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
|
||||||
136
installer/runtime-run-ocsentinel.ps1
Normal file
136
installer/runtime-run-ocsentinel.ps1
Normal file
@@ -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
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
param()
|
param(
|
||||||
|
[Parameter(ValueFromRemainingArguments = $true)]
|
||||||
|
[string[]]$RemainingArgs
|
||||||
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
$installRoot = Join-Path ${env:ProgramFiles} "OCSentinel"
|
$newScript = Join-Path $PSScriptRoot "uninstall-ocsentinel.ps1"
|
||||||
$uninstallKey = "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\OCSentinel"
|
if (-not (Test-Path $newScript)) {
|
||||||
|
throw "Replacement script not found: $newScript"
|
||||||
if (Test-Path $uninstallKey) {
|
|
||||||
Remove-Item -Path $uninstallKey -Force -Recurse
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Test-Path $installRoot) {
|
Write-Host "Compatibility wrapper: uninstall-attacktracer-ninja.ps1 -> uninstall-ocsentinel.ps1"
|
||||||
Remove-Item -LiteralPath $installRoot -Force -Recurse
|
& powershell.exe -ExecutionPolicy Bypass -File $newScript @RemainingArgs
|
||||||
}
|
exit $LASTEXITCODE
|
||||||
|
|
||||||
Write-Host "OfficeCom Sentinel removed from $installRoot"
|
|
||||||
|
|||||||
16
installer/uninstall-ocsentinel.ps1
Normal file
16
installer/uninstall-ocsentinel.ps1
Normal file
@@ -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"
|
||||||
@@ -1,131 +1,15 @@
|
|||||||
param(
|
param(
|
||||||
[string]$ManifestUrl = "",
|
[Parameter(ValueFromRemainingArguments = $true)]
|
||||||
[string]$Channel = "stable",
|
[string[]]$RemainingArgs
|
||||||
[string]$TempRoot = "$env:TEMP\OCSentinelUpdate",
|
|
||||||
[switch]$Force
|
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
$installRoot = Join-Path ${env:ProgramFiles} "OCSentinel"
|
$newScript = Join-Path $PSScriptRoot "update-ocsentinel.ps1"
|
||||||
$appExe = Join-Path $installRoot "app\OCSentinelCli.exe"
|
if (-not (Test-Path $newScript)) {
|
||||||
$installScript = Join-Path $installRoot "scripts\install-ocsentinel.ps1"
|
throw "Replacement script not found: $newScript"
|
||||||
$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 {
|
Write-Host "Compatibility wrapper: update-attacktracer-ninja.ps1 -> update-ocsentinel.ps1"
|
||||||
param(
|
& powershell.exe -ExecutionPolicy Bypass -File $newScript @RemainingArgs
|
||||||
[Parameter(Mandatory)][string]$Left,
|
exit $LASTEXITCODE
|
||||||
[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"
|
|
||||||
|
|||||||
131
installer/update-ocsentinel.ps1
Normal file
131
installer/update-ocsentinel.ps1
Normal file
@@ -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"
|
||||||
@@ -1,25 +1,15 @@
|
|||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(ValueFromRemainingArguments = $true)]
|
||||||
[string]$SecretValue,
|
[string[]]$RemainingArgs
|
||||||
[string]$OutputPath = "C:\ProgramData\OCSentinel\secrets\ocsentinel-upload-secret.dat"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
Add-Type -AssemblyName System.Security
|
$newScript = Join-Path $PSScriptRoot "protect-ocsentinel-secret.ps1"
|
||||||
|
if (-not (Test-Path $newScript)) {
|
||||||
$outputFullPath = [System.IO.Path]::GetFullPath($OutputPath)
|
throw "Replacement script not found: $newScript"
|
||||||
$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)
|
Write-Host "Compatibility wrapper: protect-attacktracer-secret.ps1 -> protect-ocsentinel-secret.ps1"
|
||||||
$protectedBytes = [System.Security.Cryptography.ProtectedData]::Protect(
|
& powershell.exe -ExecutionPolicy Bypass -File $newScript @RemainingArgs
|
||||||
$secretBytes,
|
exit $LASTEXITCODE
|
||||||
$null,
|
|
||||||
[System.Security.Cryptography.DataProtectionScope]::LocalMachine
|
|
||||||
)
|
|
||||||
|
|
||||||
[System.IO.File]::WriteAllBytes($outputFullPath, $protectedBytes)
|
|
||||||
Write-Host "Protected secret written to $outputFullPath"
|
|
||||||
|
|||||||
25
scripts/protect-ocsentinel-secret.ps1
Normal file
25
scripts/protect-ocsentinel-secret.ps1
Normal file
@@ -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"
|
||||||
@@ -1,213 +1,15 @@
|
|||||||
param(
|
param(
|
||||||
[int]$LookbackDays = 7,
|
[Parameter(ValueFromRemainingArguments = $true)]
|
||||||
[int]$TopCount = 10,
|
[string[]]$RemainingArgs
|
||||||
[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"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
$newScript = Join-Path $PSScriptRoot "run-ocsentinel-monitor.ps1"
|
||||||
$runnerScript = Join-Path $repoRoot "scripts\run-attacktracer-ninja.ps1"
|
if (-not (Test-Path $newScript)) {
|
||||||
$outputFullPath = [System.IO.Path]::GetFullPath((Join-Path $repoRoot $OutputPath))
|
throw "Replacement script not found: $newScript"
|
||||||
|
|
||||||
$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 {
|
Write-Host "Compatibility wrapper: run-attacktracer-ninja-monitor.ps1 -> run-ocsentinel-monitor.ps1"
|
||||||
if ($null -ne $script:NinjaFieldBackend) {
|
& powershell.exe -ExecutionPolicy Bypass -File $newScript @RemainingArgs
|
||||||
return
|
exit $LASTEXITCODE
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|||||||
@@ -1,144 +1,15 @@
|
|||||||
param(
|
param(
|
||||||
[int]$LookbackDays = 7,
|
[Parameter(ValueFromRemainingArguments = $true)]
|
||||||
[int]$TopCount = 10,
|
[string[]]$RemainingArgs
|
||||||
[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
|
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
$newScript = Join-Path $PSScriptRoot "run-ocsentinel.ps1"
|
||||||
$projectPath = Join-Path $repoRoot "src\AttackTracerNinjaCli\AttackTracerNinjaCli.csproj"
|
if (-not (Test-Path $newScript)) {
|
||||||
$outputFullPath = [System.IO.Path]::GetFullPath((Join-Path $repoRoot $OutputPath))
|
throw "Replacement script not found: $newScript"
|
||||||
$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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$arguments = @(
|
Write-Host "Compatibility wrapper: run-attacktracer-ninja.ps1 -> run-ocsentinel.ps1"
|
||||||
$dllPath
|
& powershell.exe -ExecutionPolicy Bypass -File $newScript @RemainingArgs
|
||||||
)
|
exit $LASTEXITCODE
|
||||||
|
|
||||||
$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
|
|
||||||
|
|||||||
213
scripts/run-ocsentinel-monitor.ps1
Normal file
213
scripts/run-ocsentinel-monitor.ps1
Normal file
@@ -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
|
||||||
144
scripts/run-ocsentinel.ps1
Normal file
144
scripts/run-ocsentinel.ps1
Normal file
@@ -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
|
||||||
@@ -4,7 +4,7 @@ internal static class ScanAndUploadCommand
|
|||||||
{
|
{
|
||||||
public static int Execute(string[] args)
|
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;
|
bool hasOutput = false;
|
||||||
|
|
||||||
for (int i = 0; i < args.Length; i++)
|
for (int i = 0; i < args.Length; i++)
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ internal static class UploadCommand
|
|||||||
}
|
}
|
||||||
|
|
||||||
string resolvedConfigPath = Path.GetFullPath(configPath ?? @"C:\ProgramData\OCSentinel\config\ocsentinel-client.json");
|
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);
|
ClientConfiguration config = ClientConfiguration.Load(resolvedConfigPath);
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(config.N8nWebhookUrl))
|
if (string.IsNullOrWhiteSpace(config.N8nWebhookUrl))
|
||||||
|
|||||||
Reference in New Issue
Block a user