Adopt OCSentinel script layout and add Gitea workflow
Some checks failed
OfficeCom Sentinel Client / build-client (push) Has been cancelled

This commit is contained in:
OfficeCom Codex
2026-07-17 00:50:26 +02:00
parent 7cdc0395c4
commit 85e394f647
23 changed files with 1124 additions and 920 deletions

View File

@@ -1,77 +1,15 @@
param()
param(
[Parameter(ValueFromRemainingArguments = $true)]
[string[]]$RemainingArgs
)
$ErrorActionPreference = "Stop"
$scriptPath = $MyInvocation.MyCommand.Path
$scriptDirectory = Split-Path -Parent $scriptPath
$packageRoot = if ((Split-Path -Leaf $scriptDirectory) -ieq "scripts") { Split-Path -Parent $scriptDirectory } else { $scriptDirectory }
$installRoot = Join-Path ${env:ProgramFiles} "OCSentinel"
$appRoot = Join-Path $installRoot "app"
$configRoot = Join-Path $installRoot "config"
$reportsRoot = Join-Path $installRoot "reports"
$samplesRoot = Join-Path $installRoot "samples"
$scriptRoot = Join-Path $installRoot "scripts"
$versionFile = Join-Path $packageRoot "VERSION.txt"
$version = if (Test-Path $versionFile) { (Get-Content $versionFile -Raw).Trim() } else { "1.0.0" }
Write-Host "Installing OfficeCom Sentinel $version to $installRoot"
New-Item -ItemType Directory -Force -Path $appRoot, $configRoot, $reportsRoot, $samplesRoot, $scriptRoot | Out-Null
Copy-Item -Path (Join-Path $packageRoot "app\OCSentinelCli.exe") -Destination $appRoot -Force
if (Test-Path (Join-Path $packageRoot "app\OCSentinelCli.pdb")) {
Copy-Item -Path (Join-Path $packageRoot "app\OCSentinelCli.pdb") -Destination $appRoot -Force
}
Copy-Item -Path (Join-Path $packageRoot "config\ocsentinel-settings.example.json") -Destination (Join-Path $configRoot "ocsentinel-settings.example.json") -Force
if (Test-Path (Join-Path $packageRoot "config\ocsentinel-client.example.json")) {
Copy-Item -Path (Join-Path $packageRoot "config\ocsentinel-client.example.json") -Destination (Join-Path $configRoot "ocsentinel-client.example.json") -Force
}
Copy-Item -Path (Join-Path $packageRoot "samples\ninja-vulnerability-export.example.csv") -Destination (Join-Path $samplesRoot "ninja-vulnerability-export.example.csv") -Force
Copy-Item -Path (Join-Path $packageRoot "scripts\run-ocsentinel.ps1") -Destination $scriptRoot -Force
Copy-Item -Path (Join-Path $packageRoot "scripts\run-ocsentinel-monitor.ps1") -Destination $scriptRoot -Force
if (Test-Path (Join-Path $packageRoot "scripts\protect-ocsentinel-secret.ps1")) {
Copy-Item -Path (Join-Path $packageRoot "scripts\protect-ocsentinel-secret.ps1") -Destination $scriptRoot -Force
}
if (Test-Path (Join-Path $packageRoot "scripts\update-ocsentinel.ps1")) {
Copy-Item -Path (Join-Path $packageRoot "scripts\update-ocsentinel.ps1") -Destination $scriptRoot -Force
}
if (Test-Path (Join-Path $packageRoot "scripts\build-attacktracer-org-report.ps1")) {
Copy-Item -Path (Join-Path $packageRoot "scripts\build-attacktracer-org-report.ps1") -Destination $scriptRoot -Force
}
Copy-Item -Path (Join-Path $packageRoot "scripts\uninstall-ocsentinel.ps1") -Destination $scriptRoot -Force
$mainConfig = Join-Path $configRoot "ocsentinel-settings.json"
$exampleConfig = Join-Path $configRoot "ocsentinel-settings.example.json"
if (-not (Test-Path $mainConfig) -and (Test-Path $exampleConfig)) {
Copy-Item $exampleConfig $mainConfig -Force
$newScript = Join-Path $PSScriptRoot "install-ocsentinel.ps1"
if (-not (Test-Path $newScript)) {
throw "Replacement script not found: $newScript"
}
$clientMainConfig = Join-Path $configRoot "ocsentinel-client.json"
$clientExampleConfig = Join-Path $configRoot "ocsentinel-client.example.json"
if (-not (Test-Path $clientMainConfig) -and (Test-Path $clientExampleConfig)) {
Copy-Item $clientExampleConfig $clientMainConfig -Force
}
$uninstallScript = Join-Path $scriptRoot "uninstall-ocsentinel.ps1"
$uninstallCommand = "powershell.exe -ExecutionPolicy Bypass -File `"$uninstallScript`""
$uninstallKey = "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\OCSentinel"
if (-not (Test-Path $uninstallKey)) {
New-Item -Path $uninstallKey -Force | Out-Null
}
Set-ItemProperty -Path $uninstallKey -Name "DisplayName" -Value "OfficeCom Sentinel"
Set-ItemProperty -Path $uninstallKey -Name "DisplayVersion" -Value $version
Set-ItemProperty -Path $uninstallKey -Name "Publisher" -Value "OfficeCom"
Set-ItemProperty -Path $uninstallKey -Name "InstallLocation" -Value $installRoot
Set-ItemProperty -Path $uninstallKey -Name "UninstallString" -Value $uninstallCommand
Set-ItemProperty -Path $uninstallKey -Name "QuietUninstallString" -Value $uninstallCommand
Set-ItemProperty -Path $uninstallKey -Name "NoModify" -Value 1 -Type DWord
Set-ItemProperty -Path $uninstallKey -Name "NoRepair" -Value 1 -Type DWord
Write-Host "Installation complete."
Write-Host "Main path: $installRoot"
Write-Host "Runner: $(Join-Path $scriptRoot 'run-ocsentinel.ps1')"
Write-Host "Monitor: $(Join-Path $scriptRoot 'run-ocsentinel-monitor.ps1')"
Write-Host "Updater: $(Join-Path $scriptRoot 'update-ocsentinel.ps1')"
Write-Host "Org report:$(Join-Path $scriptRoot 'build-attacktracer-org-report.ps1')"
Write-Host "Compatibility wrapper: install-attacktracer-ninja.ps1 -> install-ocsentinel.ps1"
& powershell.exe -ExecutionPolicy Bypass -File $newScript @RemainingArgs
exit $LASTEXITCODE

View 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')"

View File

@@ -1,3 +1,3 @@
@echo off
powershell.exe -ExecutionPolicy Bypass -File "%~dp0install-attacktracer-ninja.ps1"
powershell.exe -ExecutionPolicy Bypass -File "%~dp0install-ocsentinel.ps1"
exit /b %errorlevel%

View File

@@ -1,213 +1,15 @@
param(
[int]$LookbackDays = 7,
[int]$TopCount = 10,
[string]$OutputPath = "..\reports\ocsentinel-summary.json",
[string]$ConfigPath = "..\config\ocsentinel-settings.json",
[string]$VulnerabilityCsvPath = "",
[string]$MirrorRoot = "",
[ValidateSet("status", "attack-only", "cve-critical", "attack-plus-cve")]
[string]$Mode = "status"
[Parameter(ValueFromRemainingArguments = $true)]
[string[]]$RemainingArgs
)
$ErrorActionPreference = "Stop"
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$runnerScript = Join-Path $scriptDir "run-ocsentinel.ps1"
$outputFullPath = [System.IO.Path]::GetFullPath((Join-Path $scriptDir $OutputPath))
$script:NinjaFieldBackend = $null
$script:NinjaCliPath = "C:\ProgramData\NinjaRMMAgent\ninjarmm-cli.exe"
function Resolve-PathLike {
param(
[Parameter(Mandatory)]
[string]$PathValue,
[Parameter(Mandatory)]
[string]$BasePath
)
if ([string]::IsNullOrWhiteSpace($PathValue)) {
return $PathValue
}
if ([System.IO.Path]::IsPathRooted($PathValue) -or $PathValue.StartsWith("\\")) {
return [System.IO.Path]::GetFullPath($PathValue)
}
return [System.IO.Path]::GetFullPath((Join-Path $BasePath $PathValue))
$newScript = Join-Path $PSScriptRoot "runtime-run-ocsentinel-monitor.ps1"
if (-not (Test-Path $newScript)) {
throw "Replacement script not found: $newScript"
}
function Initialize-NinjaFieldWriter {
if ($null -ne $script:NinjaFieldBackend) {
return
}
if (Get-Command -Name "Ninja-Property-Set" -ErrorAction SilentlyContinue) {
$script:NinjaFieldBackend = "powershell"
return
}
if (Test-Path $script:NinjaCliPath) {
$script:NinjaFieldBackend = "cli"
return
}
$script:NinjaFieldBackend = "none"
}
function Set-NinjaCustomFieldValue {
param(
[Parameter(Mandatory)]
[string]$Name,
[AllowEmptyString()]
[string]$Value
)
Initialize-NinjaFieldWriter
switch ($script:NinjaFieldBackend) {
"powershell" {
Ninja-Property-Set $Name $Value | Out-Null
return $true
}
"cli" {
& $script:NinjaCliPath set $Name $Value | Out-Null
return $LASTEXITCODE -eq 0
}
default {
return $false
}
}
}
function Publish-NinjaCustomFields {
param(
[Parameter(Mandatory)]
[pscustomobject]$Report,
[Parameter(Mandatory)]
[string]$Mode,
[Parameter(Mandatory)]
[bool]$Triggered,
[Parameter(Mandatory)]
[string]$Reason
)
Initialize-NinjaFieldWriter
if ($script:NinjaFieldBackend -eq "none") {
Write-Host "Ninja custom fields: skipped (Ninja field writer not available)."
return
}
$generatedAtUtc = ""
if ($Report.GeneratedAtLocal) {
try {
$generatedAtUtc = ([DateTimeOffset]$Report.GeneratedAtLocal).ToUniversalTime().ToString("o")
}
catch {
$generatedAtUtc = [string]$Report.GeneratedAtLocal
}
}
$fieldValues = [ordered]@{
"ocsentinelstatus" = [string]$Report.AlertState
"ocsentinelreason" = $Reason
"ocsentinelbasestatus" = [string]$Report.BaseAlertState
"ocsentinelevents" = [string]([int]$Report.TotalEvents)
"ocsentineluniqueips" = [string]([int]$Report.UniqueIpCount)
"ocsentinelcvecritical" = [string]([int]$Report.VulnerabilityCorrelation.CriticalCount)
"ocsentinelcvetotal" = [string]([int]$Report.VulnerabilityCorrelation.TotalCount)
"ocsentinelmode" = $Mode
"ocsentineltriggered" = $Triggered.ToString().ToLowerInvariant()
"ocsentinellastscanutc" = $generatedAtUtc
}
$updated = 0
foreach ($entry in $fieldValues.GetEnumerator()) {
try {
if (Set-NinjaCustomFieldValue -Name $entry.Key -Value $entry.Value) {
$updated++
}
}
catch {
Write-Warning "Failed to set Ninja custom field '$($entry.Key)': $($_.Exception.Message)"
}
}
Write-Host "Ninja custom fields: updated $updated field(s) via $script:NinjaFieldBackend."
}
$runnerArgs = @(
"-ExecutionPolicy", "Bypass",
"-File", $runnerScript,
"-LookbackDays", $LookbackDays,
"-TopCount", $TopCount,
"-OutputPath", $OutputPath,
"-ConfigPath", $ConfigPath
)
if (-not [string]::IsNullOrWhiteSpace($VulnerabilityCsvPath)) {
$runnerArgs += @("-VulnerabilityCsvPath", $VulnerabilityCsvPath)
}
if (-not [string]::IsNullOrWhiteSpace($MirrorRoot)) {
$runnerArgs += @("-MirrorRoot", (Resolve-PathLike -PathValue $MirrorRoot -BasePath $scriptDir))
}
$null = & powershell @runnerArgs
$runnerExitCode = $LASTEXITCODE
if (-not (Test-Path $outputFullPath)) {
throw "Expected report file was not created: $outputFullPath"
}
$report = Get-Content $outputFullPath -Raw | ConvertFrom-Json
$status = [string]$report.AlertState
$baseStatus = [string]$report.BaseAlertState
$events = [int]$report.TotalEvents
$uniqueIps = [int]$report.UniqueIpCount
$criticalCves = [int]$report.VulnerabilityCorrelation.CriticalCount
$totalCves = [int]$report.VulnerabilityCorrelation.TotalCount
$monitorTriggered = $false
$monitorReason = ""
switch ($Mode) {
"status" {
$monitorTriggered = $status -ne "ok"
$monitorReason = "Final status is $status. $($report.AlertReason)"
}
"attack-only" {
$monitorTriggered = $baseStatus -ne "ok"
$monitorReason = "Base attack status is $baseStatus. $($report.BaseAlertReason)"
}
"cve-critical" {
$monitorTriggered = $criticalCves -gt 0
$monitorReason = "Critical/high CVE count is $criticalCves out of total CVEs $totalCves."
}
"attack-plus-cve" {
$monitorTriggered = ($events -gt 0 -and $criticalCves -gt 0)
$monitorReason = "Attack events=$events and critical/high CVEs=$criticalCves."
}
}
Publish-NinjaCustomFields -Report $report -Mode $Mode -Triggered $monitorTriggered -Reason $monitorReason
Write-Host ""
Write-Host "OfficeCom Sentinel monitor mode: $Mode"
Write-Host "Triggered: $monitorTriggered"
Write-Host "Reason: $monitorReason"
Write-Host "Status: $status"
Write-Host "Base status: $baseStatus"
Write-Host "Events: $events"
Write-Host "Unique IPs: $uniqueIps"
Write-Host "Critical/High CVEs: $criticalCves"
Write-Host "Total CVEs: $totalCves"
Write-Host "Report: $outputFullPath"
Write-Host "Runner exit code: $runnerExitCode"
if ($monitorTriggered) {
exit 1
}
exit 0
Write-Host "Compatibility wrapper: runtime-run-attacktracer-ninja-monitor.ps1 -> runtime-run-ocsentinel-monitor.ps1"
& powershell.exe -ExecutionPolicy Bypass -File $newScript @RemainingArgs
exit $LASTEXITCODE

View File

@@ -1,136 +1,15 @@
param(
[int]$LookbackDays = 7,
[int]$TopCount = 10,
[string]$OutputPath = "..\reports\ocsentinel-summary.json",
[string]$ConfigPath = "..\config\ocsentinel-settings.json",
[string]$ClientConfigPath = "..\config\ocsentinel-client.json",
[string]$SecretPath = "",
[string]$VulnerabilityCsvPath = "",
[string]$MirrorRoot = "",
[ValidateSet("disabled", "auto", "required")]
[string]$UploadMode = "auto",
[switch]$FailOnAttacks,
[switch]$FailOnThreshold
[Parameter(ValueFromRemainingArguments = $true)]
[string[]]$RemainingArgs
)
$ErrorActionPreference = "Stop"
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$installRoot = Split-Path -Parent $scriptDir
$appExe = Join-Path $installRoot "app\OCSentinelCli.exe"
$outputFullPath = [System.IO.Path]::GetFullPath((Join-Path $scriptDir $OutputPath))
function Resolve-PathLike {
param(
[Parameter(Mandatory)]
[string]$PathValue,
[Parameter(Mandatory)]
[string]$BasePath
)
if ([string]::IsNullOrWhiteSpace($PathValue)) {
return $PathValue
}
if ([System.IO.Path]::IsPathRooted($PathValue) -or $PathValue.StartsWith("\\")) {
return [System.IO.Path]::GetFullPath($PathValue)
}
return [System.IO.Path]::GetFullPath((Join-Path $BasePath $PathValue))
$newScript = Join-Path $PSScriptRoot "runtime-run-ocsentinel.ps1"
if (-not (Test-Path $newScript)) {
throw "Replacement script not found: $newScript"
}
if (-not (Test-Path $appExe)) {
throw "Application executable not found: $appExe"
}
$arguments = @()
$configFullPath = [System.IO.Path]::GetFullPath((Join-Path $scriptDir $ConfigPath))
if ($UploadMode -eq "disabled") {
$arguments += "scan"
}
else {
$clientConfigFullPath = Resolve-PathLike -PathValue $ClientConfigPath -BasePath $scriptDir
$secretFullPath = if ([string]::IsNullOrWhiteSpace($SecretPath)) { "" } else { Resolve-PathLike -PathValue $SecretPath -BasePath $scriptDir }
$canUpload = (Test-Path $clientConfigFullPath) -and (-not [string]::IsNullOrWhiteSpace($secretFullPath)) -and (Test-Path $secretFullPath)
if ($UploadMode -eq "required" -and -not $canUpload) {
throw "UploadMode 'required' was set, but client config or protected secret is missing."
}
if ($canUpload) {
$arguments += "scan-and-upload"
$arguments += @("--client-config", $clientConfigFullPath, "--secret-path", $secretFullPath)
}
else {
$arguments += "scan"
}
}
$arguments += @(
"--lookback-days", $LookbackDays,
"--top", $TopCount,
"--output", $outputFullPath,
"--ninja-output"
)
if (Test-Path $configFullPath) {
$arguments += @("--config", $configFullPath)
}
if (-not [string]::IsNullOrWhiteSpace($VulnerabilityCsvPath)) {
$vulnerabilityCsvFullPath = Resolve-PathLike -PathValue $VulnerabilityCsvPath -BasePath $scriptDir
if (Test-Path $vulnerabilityCsvFullPath) {
$arguments += @("--vulnerability-csv", $vulnerabilityCsvFullPath)
}
}
if ($FailOnAttacks) {
$arguments += "--fail-on-attacks"
}
if ($FailOnThreshold) {
$arguments += "--fail-on-threshold"
}
& $appExe @arguments
$exitCode = $LASTEXITCODE
if (-not (Test-Path $outputFullPath)) {
throw "Expected report file was not created: $outputFullPath"
}
$report = Get-Content $outputFullPath -Raw | ConvertFrom-Json
if (-not [string]::IsNullOrWhiteSpace($MirrorRoot)) {
Write-Host "Legacy mirror mode enabled."
$mirrorRootPath = Resolve-PathLike -PathValue $MirrorRoot -BasePath $scriptDir
if (-not (Test-Path $mirrorRootPath)) {
New-Item -ItemType Directory -Force -Path $mirrorRootPath | Out-Null
}
$mirrorPath = Join-Path $mirrorRootPath "$($report.MachineName).json"
Copy-Item -Path $outputFullPath -Destination $mirrorPath -Force
Write-Host "Mirrored report: $mirrorPath"
}
Write-Host ""
Write-Host "OfficeCom Sentinel runner summary"
Write-Host "Machine: $($report.MachineName)"
Write-Host "Events: $($report.TotalEvents)"
Write-Host "Unique IPs: $($report.UniqueIpCount)"
Write-Host "Status: $($report.AlertState)"
Write-Host "Reason: $($report.AlertReason)"
Write-Host "Base status: $($report.BaseAlertState)"
Write-Host "CVE findings: $($report.VulnerabilityCorrelation.TotalCount)"
Write-Host "Critical/High CVEs: $($report.VulnerabilityCorrelation.CriticalCount)"
Write-Host "Upload mode: $UploadMode"
Write-Host "Report: $outputFullPath"
if ($report.Errors.Count -gt 0) {
Write-Host "Warnings:"
foreach ($warningEntry in $report.Errors) {
Write-Host "- $warningEntry"
}
}
exit $exitCode
Write-Host "Compatibility wrapper: runtime-run-attacktracer-ninja.ps1 -> runtime-run-ocsentinel.ps1"
& powershell.exe -ExecutionPolicy Bypass -File $newScript @RemainingArgs
exit $LASTEXITCODE

View 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

View 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

View File

@@ -1,16 +1,15 @@
param()
param(
[Parameter(ValueFromRemainingArguments = $true)]
[string[]]$RemainingArgs
)
$ErrorActionPreference = "Stop"
$installRoot = Join-Path ${env:ProgramFiles} "OCSentinel"
$uninstallKey = "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\OCSentinel"
if (Test-Path $uninstallKey) {
Remove-Item -Path $uninstallKey -Force -Recurse
$newScript = Join-Path $PSScriptRoot "uninstall-ocsentinel.ps1"
if (-not (Test-Path $newScript)) {
throw "Replacement script not found: $newScript"
}
if (Test-Path $installRoot) {
Remove-Item -LiteralPath $installRoot -Force -Recurse
}
Write-Host "OfficeCom Sentinel removed from $installRoot"
Write-Host "Compatibility wrapper: uninstall-attacktracer-ninja.ps1 -> uninstall-ocsentinel.ps1"
& powershell.exe -ExecutionPolicy Bypass -File $newScript @RemainingArgs
exit $LASTEXITCODE

View 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"

View File

@@ -1,131 +1,15 @@
param(
[string]$ManifestUrl = "",
[string]$Channel = "stable",
[string]$TempRoot = "$env:TEMP\OCSentinelUpdate",
[switch]$Force
[Parameter(ValueFromRemainingArguments = $true)]
[string[]]$RemainingArgs
)
$ErrorActionPreference = "Stop"
$installRoot = Join-Path ${env:ProgramFiles} "OCSentinel"
$appExe = Join-Path $installRoot "app\OCSentinelCli.exe"
$installScript = Join-Path $installRoot "scripts\install-ocsentinel.ps1"
$versionFile = Join-Path $installRoot "VERSION.txt"
function Get-InstalledVersion {
if (Test-Path $versionFile) {
return (Get-Content $versionFile -Raw).Trim()
}
if (Test-Path $appExe) {
return (Get-Item $appExe).VersionInfo.ProductVersion
}
return "0.0.0"
$newScript = Join-Path $PSScriptRoot "update-ocsentinel.ps1"
if (-not (Test-Path $newScript)) {
throw "Replacement script not found: $newScript"
}
function Compare-Version {
param(
[Parameter(Mandatory)][string]$Left,
[Parameter(Mandatory)][string]$Right
)
try {
$leftVersion = [System.Version]$Left
$rightVersion = [System.Version]$Right
return $leftVersion.CompareTo($rightVersion)
}
catch {
return [string]::Compare($Left, $Right, $true)
}
}
function Get-Sha256Hex {
param([Parameter(Mandatory)][string]$Path)
return (Get-FileHash -Path $Path -Algorithm SHA256).Hash.ToLowerInvariant()
}
function Resolve-ManifestUrl {
param(
[Parameter(Mandatory)][string]$ManifestUrl,
[Parameter(Mandatory)][string]$Channel
)
if ($ManifestUrl -match '\.json($|\?)') {
return $ManifestUrl
}
return ($ManifestUrl.TrimEnd('/') + "/$Channel/version.json")
}
if ([string]::IsNullOrWhiteSpace($ManifestUrl)) {
throw "ManifestUrl is required."
}
$resolvedManifestUrl = Resolve-ManifestUrl -ManifestUrl $ManifestUrl -Channel $Channel
Write-Host "Checking update manifest: $resolvedManifestUrl"
$manifest = Invoke-RestMethod -Method Get -Uri $resolvedManifestUrl -TimeoutSec 60
if (-not $manifest.version -or -not $manifest.artifactUrl -or -not $manifest.sha256) {
throw "Update manifest is missing required fields: version, artifactUrl, sha256."
}
$installedVersion = Get-InstalledVersion
$availableVersion = [string]$manifest.version
Write-Host "Installed version: $installedVersion"
Write-Host "Available version: $availableVersion"
if (-not $Force -and (Compare-Version -Left $installedVersion -Right $availableVersion) -ge 0) {
Write-Host "OfficeCom Sentinel is already up to date."
exit 0
}
$downloadRoot = Join-Path $TempRoot ([Guid]::NewGuid().ToString("N"))
$zipPath = Join-Path $downloadRoot "OCSentinelClient.zip"
$extractRoot = Join-Path $downloadRoot "payload"
New-Item -ItemType Directory -Force -Path $downloadRoot, $extractRoot | Out-Null
Write-Host "Downloading artifact: $($manifest.artifactUrl)"
Invoke-WebRequest -Uri ([string]$manifest.artifactUrl) -OutFile $zipPath -TimeoutSec 300
$actualHash = Get-Sha256Hex -Path $zipPath
$expectedHash = ([string]$manifest.sha256).ToLowerInvariant()
if ($actualHash -ne $expectedHash) {
throw "SHA-256 mismatch for downloaded artifact. Expected $expectedHash but got $actualHash."
}
Write-Host "Artifact hash verified"
Expand-Archive -Path $zipPath -DestinationPath $extractRoot -Force
$payloadAppExe = Get-ChildItem -Path $extractRoot -Recurse -Filter "OCSentinelCli.exe" | Select-Object -First 1
if ($null -eq $payloadAppExe) {
throw "Downloaded payload did not contain OCSentinelCli.exe"
}
$signature = Get-AuthenticodeSignature -FilePath $payloadAppExe.FullName
if ($signature.Status -notin @("Valid", "NotSigned")) {
throw "Executable signature validation failed with status: $($signature.Status)"
}
if ($signature.Status -eq "NotSigned") {
Write-Warning "Downloaded executable is not code-signed yet. Hash validation succeeded, but code signing should be added before production rollout."
}
else {
Write-Host "Executable signature verified: $($signature.SignerCertificate.Subject)"
}
$payloadInstallScript = Get-ChildItem -Path $extractRoot -Recurse -Filter "install-ocsentinel.ps1" | Select-Object -First 1
if ($null -eq $payloadInstallScript) {
throw "Downloaded payload did not contain install-ocsentinel.ps1"
}
Write-Host "Installing OfficeCom Sentinel $availableVersion"
& powershell.exe -ExecutionPolicy Bypass -File $payloadInstallScript.FullName
if ($LASTEXITCODE -ne 0) {
throw "Installer exited with code $LASTEXITCODE"
}
Write-Host "Update complete: $installedVersion -> $availableVersion"
Write-Host "Compatibility wrapper: update-attacktracer-ninja.ps1 -> update-ocsentinel.ps1"
& powershell.exe -ExecutionPolicy Bypass -File $newScript @RemainingArgs
exit $LASTEXITCODE

View 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"