diff --git a/build/build-client-package.ps1 b/build/build-client-package.ps1
index 04ffc2d..cf142d9 100644
--- a/build/build-client-package.ps1
+++ b/build/build-client-package.ps1
@@ -54,6 +54,7 @@ Copy-Item -Path (Join-Path $installerRoot "uninstall-ocsentinel.ps1") -Destinati
Copy-Item -Path (Join-Path $installerRoot "update-ocsentinel.ps1") -Destination (Join-Path $packageRoot "scripts\update-ocsentinel.ps1") -Force
Copy-Item -Path (Join-Path $installerRoot "runtime-run-ocsentinel.ps1") -Destination (Join-Path $packageRoot "scripts\run-ocsentinel.ps1") -Force
Copy-Item -Path (Join-Path $installerRoot "runtime-run-ocsentinel-monitor.ps1") -Destination (Join-Path $packageRoot "scripts\run-ocsentinel-monitor.ps1") -Force
+Copy-Item -Path (Join-Path $installerRoot "runtime-run-ocsentinel-scheduled.ps1") -Destination (Join-Path $packageRoot "scripts\run-ocsentinel-scheduled.ps1") -Force
Copy-Item -Path (Join-Path $repoRoot "scripts\protect-ocsentinel-secret.ps1") -Destination (Join-Path $packageRoot "scripts\protect-ocsentinel-secret.ps1") -Force
Copy-Item -Path (Join-Path $repoRoot "config\ocsentinel-settings.example.json") -Destination (Join-Path $packageRoot "config\ocsentinel-settings.example.json") -Force
diff --git a/docs/ocsentinel-deployment.md b/docs/ocsentinel-deployment.md
index d020c9f..288ae42 100644
--- a/docs/ocsentinel-deployment.md
+++ b/docs/ocsentinel-deployment.md
@@ -30,6 +30,19 @@ powershell -ExecutionPolicy Bypass -File .\build\build-release-manifest.ps1 `
- `config\ocsentinel-settings.json`
- `config\ocsentinel-client.json`
+## Local Schedule And Burst Mode
+
+The installer creates two Windows Scheduled Tasks running as `SYSTEM`:
+
+- `OCSentinel Daily Scan`: runs every day at `08:00` and uploads one signed report.
+- `OCSentinel Burst Check`: runs every five minutes. It performs no scan unless
+ the NinjaOne device custom field `ocsentinelburst` is enabled.
+
+Create `ocsentinelburst` as a device-level `Checkbox` custom field and allow
+automation read access. Set it to `true` for a device to begin the five-minute
+burst scans; clear it to stop them. The normal daily scan continues regardless
+of the checkbox.
+
## NinjaOne Tasks
Create a PowerShell script in NinjaOne named `OCSentinel - Installieren oder aktualisieren`.
diff --git a/installer/install-ocsentinel.ps1 b/installer/install-ocsentinel.ps1
index 40374b0..3ba42fb 100644
--- a/installer/install-ocsentinel.ps1
+++ b/installer/install-ocsentinel.ps1
@@ -32,6 +32,7 @@ if (Test-Path (Join-Path $packageRoot "config\ocsentinel-client.dev.example.json
Copy-Item -Path (Join-Path $packageRoot "samples\ninja-vulnerability-export.example.csv") -Destination (Join-Path $samplesRoot "ninja-vulnerability-export.example.csv") -Force
Copy-Item -Path (Join-Path $packageRoot "scripts\run-ocsentinel.ps1") -Destination $scriptRoot -Force
Copy-Item -Path (Join-Path $packageRoot "scripts\run-ocsentinel-monitor.ps1") -Destination $scriptRoot -Force
+Copy-Item -Path (Join-Path $packageRoot "scripts\run-ocsentinel-scheduled.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
}
@@ -69,8 +70,21 @@ Set-ItemProperty -Path $uninstallKey -Name "QuietUninstallString" -Value $uninst
Set-ItemProperty -Path $uninstallKey -Name "NoModify" -Value 1 -Type DWord
Set-ItemProperty -Path $uninstallKey -Name "NoRepair" -Value 1 -Type DWord
+$scheduledScript = Join-Path $scriptRoot "run-ocsentinel-scheduled.ps1"
+$taskPrincipal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
+$taskSettings = New-ScheduledTaskSettingsSet -StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Minutes 30) -MultipleInstances IgnoreNew
+
+$dailyAction = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$scheduledScript`" -Kind daily" -WorkingDirectory $scriptRoot
+$dailyTrigger = New-ScheduledTaskTrigger -Daily -At 08:00
+Register-ScheduledTask -TaskName "OCSentinel Daily Scan" -Action $dailyAction -Trigger $dailyTrigger -Principal $taskPrincipal -Settings $taskSettings -Description "OfficeCom Sentinel daily signed scan and upload." -Force | Out-Null
+
+$burstAction = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$scheduledScript`" -Kind burst" -WorkingDirectory $scriptRoot
+$burstTrigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(2) -RepetitionInterval (New-TimeSpan -Minutes 5) -RepetitionDuration (New-TimeSpan -Days 3650)
+Register-ScheduledTask -TaskName "OCSentinel Burst Check" -Action $burstAction -Trigger $burstTrigger -Principal $taskPrincipal -Settings $taskSettings -Description "OfficeCom Sentinel burst check; scans only when Ninja field ocsentinelburst is enabled." -Force | Out-Null
+
Write-Host "Installation complete."
Write-Host "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 "Schedule: Daily scan at 08:00; burst check every 5 minutes."
diff --git a/installer/runtime-run-ocsentinel-monitor.ps1 b/installer/runtime-run-ocsentinel-monitor.ps1
index 760add7..0186a54 100644
--- a/installer/runtime-run-ocsentinel-monitor.ps1
+++ b/installer/runtime-run-ocsentinel-monitor.ps1
@@ -9,6 +9,7 @@ param(
[string]$UploadMode = "auto",
[string]$VulnerabilityCsvPath = "",
[string]$MirrorRoot = "",
+ [switch]$SuppressTriggerExit,
[ValidateSet("status", "attack-only", "cve-critical", "attack-plus-cve")]
[string]$Mode = "status"
)
@@ -227,8 +228,8 @@ Write-Host "Total CVEs: $totalCves"
Write-Host "Report: $outputFullPath"
Write-Host "Runner exit code: $runnerExitCode"
-if ($monitorTriggered) {
+if ($monitorTriggered -and -not $SuppressTriggerExit) {
exit 1
}
-exit 0
+exit $runnerExitCode
diff --git a/installer/runtime-run-ocsentinel-scheduled.ps1 b/installer/runtime-run-ocsentinel-scheduled.ps1
new file mode 100644
index 0000000..f48aecb
--- /dev/null
+++ b/installer/runtime-run-ocsentinel-scheduled.ps1
@@ -0,0 +1,50 @@
+[CmdletBinding()]
+param(
+ [ValidateSet("daily", "burst")]
+ [string]$Kind = "daily"
+)
+
+$ErrorActionPreference = "Stop"
+
+$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
+$monitorScript = Join-Path $scriptDir "run-ocsentinel-monitor.ps1"
+$secretPath = "C:\ProgramData\OCSentinel\secrets\ocsentinel-upload-secret.dat"
+$mutexName = "Global\OfficeComSentinelScan"
+
+function Get-NinjaBurstEnabled {
+ if (Get-Command -Name "Get-NinjaProperty" -ErrorAction SilentlyContinue) {
+ return [bool](Get-NinjaProperty -Name "ocsentinelburst" -Type "Checkbox")
+ }
+
+ if (Get-Command -Name "Ninja-Property-Get" -ErrorAction SilentlyContinue) {
+ $value = Ninja-Property-Get -Name "ocsentinelburst"
+ return [string]$value -match "^(1|true|yes)$"
+ }
+
+ Write-Warning "Ninja custom-field reader is unavailable; burst scan skipped."
+ return $false
+}
+
+if ($Kind -eq "burst" -and -not (Get-NinjaBurstEnabled)) {
+ Write-Host "OfficeCom Sentinel burst check: disabled."
+ exit 0
+}
+
+$createdNew = $false
+$mutex = [Threading.Mutex]::new($false, $mutexName, [ref]$createdNew)
+try {
+ if (-not $mutex.WaitOne(0)) {
+ Write-Host "OfficeCom Sentinel scan skipped: another scan is already running."
+ exit 0
+ }
+
+ Write-Host "OfficeCom Sentinel scheduled $Kind scan started."
+ & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $monitorScript -Mode status -UploadMode required -SecretPath $secretPath -SuppressTriggerExit
+ exit $LASTEXITCODE
+}
+finally {
+ if ($null -ne $mutex) {
+ try { $mutex.ReleaseMutex() } catch { }
+ $mutex.Dispose()
+ }
+}
diff --git a/installer/uninstall-ocsentinel.ps1 b/installer/uninstall-ocsentinel.ps1
index ab9667d..48dcdda 100644
--- a/installer/uninstall-ocsentinel.ps1
+++ b/installer/uninstall-ocsentinel.ps1
@@ -5,6 +5,12 @@ $ErrorActionPreference = "Stop"
$installRoot = Join-Path ${env:ProgramFiles} "OCSentinel"
$uninstallKey = "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\OCSentinel"
+foreach ($taskName in @("OCSentinel Daily Scan", "OCSentinel Burst Check")) {
+ if (Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue) {
+ Unregister-ScheduledTask -TaskName $taskName -Confirm:$false
+ }
+}
+
if (Test-Path $uninstallKey) {
Remove-Item -Path $uninstallKey -Force -Recurse
}
diff --git a/release/stable/version.json b/release/stable/version.json
index 2acb3d8..29a1d8f 100644
--- a/release/stable/version.json
+++ b/release/stable/version.json
@@ -1,8 +1,8 @@
{
"channel": "stable",
- "version": "1.2.11",
- "publishedAtUtc": "2026-07-25T19:17:43.7567344Z",
- "artifactUrl": "https://gitea.officecom.cloud/officecom/oc-sentinel/releases/download/v1.2.11/OCSentinelClient-win-x64.zip",
- "sha256": "4a5415ce281f444a987a3fd72980c159afaa37787bbaa396f42d2c496736e5e1",
+ "version": "1.3.0",
+ "publishedAtUtc": "2026-07-26T00:16:43.0355921Z",
+ "artifactUrl": "https://gitea.officecom.cloud/officecom/oc-sentinel/releases/download/v1.3.0/OCSentinelClient-win-x64.zip",
+ "sha256": "35dfc6e1022da0e7c4ae26d656c6c588365fd152deb5b29c1ff2d5ea95c8ac56",
"minUpdaterVersion": "1.0.0"
}
diff --git a/scripts/run-ocsentinel-monitor.ps1 b/scripts/run-ocsentinel-monitor.ps1
index d1b447e..58b838d 100644
--- a/scripts/run-ocsentinel-monitor.ps1
+++ b/scripts/run-ocsentinel-monitor.ps1
@@ -9,6 +9,7 @@ param(
[string]$UploadMode = "auto",
[string]$VulnerabilityCsvPath = "",
[string]$MirrorRoot = "",
+ [switch]$SuppressTriggerExit,
[ValidateSet("status", "attack-only", "cve-critical", "attack-plus-cve")]
[string]$Mode = "status"
)
@@ -227,8 +228,8 @@ Write-Host "Total CVEs: $totalCves"
Write-Host "Report: $outputFullPath"
Write-Host "Runner exit code: $runnerExitCode"
-if ($monitorTriggered) {
+if ($monitorTriggered -and -not $SuppressTriggerExit) {
exit 1
}
-exit 0
+exit $runnerExitCode
diff --git a/scripts/run-ocsentinel-scheduled.ps1 b/scripts/run-ocsentinel-scheduled.ps1
new file mode 100644
index 0000000..f48aecb
--- /dev/null
+++ b/scripts/run-ocsentinel-scheduled.ps1
@@ -0,0 +1,50 @@
+[CmdletBinding()]
+param(
+ [ValidateSet("daily", "burst")]
+ [string]$Kind = "daily"
+)
+
+$ErrorActionPreference = "Stop"
+
+$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
+$monitorScript = Join-Path $scriptDir "run-ocsentinel-monitor.ps1"
+$secretPath = "C:\ProgramData\OCSentinel\secrets\ocsentinel-upload-secret.dat"
+$mutexName = "Global\OfficeComSentinelScan"
+
+function Get-NinjaBurstEnabled {
+ if (Get-Command -Name "Get-NinjaProperty" -ErrorAction SilentlyContinue) {
+ return [bool](Get-NinjaProperty -Name "ocsentinelburst" -Type "Checkbox")
+ }
+
+ if (Get-Command -Name "Ninja-Property-Get" -ErrorAction SilentlyContinue) {
+ $value = Ninja-Property-Get -Name "ocsentinelburst"
+ return [string]$value -match "^(1|true|yes)$"
+ }
+
+ Write-Warning "Ninja custom-field reader is unavailable; burst scan skipped."
+ return $false
+}
+
+if ($Kind -eq "burst" -and -not (Get-NinjaBurstEnabled)) {
+ Write-Host "OfficeCom Sentinel burst check: disabled."
+ exit 0
+}
+
+$createdNew = $false
+$mutex = [Threading.Mutex]::new($false, $mutexName, [ref]$createdNew)
+try {
+ if (-not $mutex.WaitOne(0)) {
+ Write-Host "OfficeCom Sentinel scan skipped: another scan is already running."
+ exit 0
+ }
+
+ Write-Host "OfficeCom Sentinel scheduled $Kind scan started."
+ & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $monitorScript -Mode status -UploadMode required -SecretPath $secretPath -SuppressTriggerExit
+ exit $LASTEXITCODE
+}
+finally {
+ if ($null -ne $mutex) {
+ try { $mutex.ReleaseMutex() } catch { }
+ $mutex.Dispose()
+ }
+}
diff --git a/src/OCSentinelCli/OCSentinelCli.csproj b/src/OCSentinelCli/OCSentinelCli.csproj
index 0668ab5..e7488e1 100644
--- a/src/OCSentinelCli/OCSentinelCli.csproj
+++ b/src/OCSentinelCli/OCSentinelCli.csproj
@@ -9,10 +9,10 @@
OCSentinelCli
OfficeCom Sentinel
OfficeCom
- 1.2.11
- 1.2.11.0
- 1.2.11.0
- 1.2.11
+ 1.3.0
+ 1.3.0.0
+ 1.3.0.0
+ 1.3.0