25 Commits

Author SHA1 Message Date
OfficeCom Codex
f91f45ad92 Prepare automated release validation
Some checks failed
OfficeCom Sentinel Client / validate-client (push) Successful in 23s
OfficeCom Sentinel Client / build-client-windows (push) Failing after 17s
2026-07-25 10:25:07 +02:00
OfficeCom Codex
c27b53ea0d Use repository secret for Gitea releases
Some checks failed
OfficeCom Sentinel Client / build-client-windows (push) Has been cancelled
OfficeCom Sentinel Client / validate-client (push) Has been cancelled
2026-07-25 10:24:56 +02:00
OfficeCom Codex
f9d7647046 Detect Gitea release tags by reference name
Some checks failed
OfficeCom Sentinel Client / build-client-windows (push) Failing after 18s
OfficeCom Sentinel Client / validate-client (push) Successful in 23s
2026-07-25 10:22:43 +02:00
OfficeCom Codex
d37fba137e Run Gitea release pipeline from tag environment
Some checks failed
OfficeCom Sentinel Client / validate-client (push) Successful in 23s
OfficeCom Sentinel Client / build-client-windows (push) Failing after 18s
2026-07-25 10:20:55 +02:00
OfficeCom Codex
e73d79b520 Publish stable manifest for v1.2.6
All checks were successful
OfficeCom Sentinel Client / validate-client (push) Successful in 22s
OfficeCom Sentinel Client / build-client-windows (push) Has been skipped
2026-07-25 10:16:52 +02:00
OfficeCom Codex
b0e3e7dc74 Preserve upload configuration during scan and upload
Some checks failed
OfficeCom Sentinel Client / validate-client (push) Successful in 23s
OfficeCom Sentinel Client / build-client-windows (push) Failing after 19s
2026-07-25 10:15:23 +02:00
OfficeCom Codex
290033680b Publish release assets from tag workflow
All checks were successful
OfficeCom Sentinel Client / validate-client (push) Successful in 25s
OfficeCom Sentinel Client / build-client-windows (push) Has been skipped
2026-07-25 03:08:12 +02:00
OfficeCom Codex
3ea0baa147 Build Windows client packages on Linux runner
All checks were successful
OfficeCom Sentinel Client / validate-client (push) Successful in 24s
OfficeCom Sentinel Client / build-client-windows (push) Has been skipped
2026-07-25 03:06:31 +02:00
OfficeCom Codex
e01aa3dce3 Target Windows Gitea runner label
Some checks failed
OfficeCom Sentinel Client / validate-client (push) Successful in 25s
OfficeCom Sentinel Client / build-client-windows (push) Has been cancelled
2026-07-25 02:43:12 +02:00
OfficeCom Codex
8bc1d78fc9 Publish stable manifest for v1.2.5
Some checks failed
OfficeCom Sentinel Client / validate-client (push) Successful in 24s
OfficeCom Sentinel Client / build-client-windows (push) Has been cancelled
2026-07-25 02:40:40 +02:00
OfficeCom Codex
00778175fd Include NinjaOne identifiers in reports
Some checks failed
OfficeCom Sentinel Client / validate-client (push) Successful in 25s
OfficeCom Sentinel Client / build-client-windows (push) Has been cancelled
2026-07-25 02:39:39 +02:00
OfficeCom Codex
34eff4e012 Publish stable manifest for v1.2.4
Some checks failed
OfficeCom Sentinel Client / validate-client (push) Successful in 24s
OfficeCom Sentinel Client / build-client-windows (push) Has been cancelled
2026-07-25 02:35:52 +02:00
OfficeCom Codex
707275f992 Add signed report upload to client runtime
Some checks failed
OfficeCom Sentinel Client / validate-client (push) Successful in 24s
OfficeCom Sentinel Client / build-client-windows (push) Has been cancelled
2026-07-25 02:34:57 +02:00
OfficeCom Codex
b1ee79ca02 Configure upload during Ninja bootstrap
Some checks failed
OfficeCom Sentinel Client / validate-client (push) Successful in 25s
OfficeCom Sentinel Client / build-client-windows (push) Has been cancelled
2026-07-25 02:33:02 +02:00
OfficeCom Codex
053d601e93 Add internal development webhook configuration
Some checks failed
OfficeCom Sentinel Client / validate-client (push) Successful in 24s
OfficeCom Sentinel Client / build-client-windows (push) Has been cancelled
2026-07-25 02:13:13 +02:00
OfficeCom Codex
67a14c125e Document Dockge PostgreSQL deployment
Some checks failed
OfficeCom Sentinel Client / validate-client (push) Successful in 25s
OfficeCom Sentinel Client / build-client-windows (push) Has been cancelled
2026-07-25 01:56:49 +02:00
OfficeCom Codex
335410b418 Add PostgreSQL reporting schema and ingest contract
Some checks failed
OfficeCom Sentinel Client / validate-client (push) Successful in 25s
OfficeCom Sentinel Client / build-client-windows (push) Has been cancelled
2026-07-25 01:41:13 +02:00
OfficeCom Codex
f9f4d862bf Add NinjaOne bootstrap deployment script
Some checks failed
OfficeCom Sentinel Client / validate-client (push) Successful in 24s
OfficeCom Sentinel Client / build-client-windows (push) Has been cancelled
2026-07-25 01:34:52 +02:00
OfficeCom Codex
ea90ddd1f6 Add restricted Windows Gitea runner setup
Some checks failed
OfficeCom Sentinel Client / validate-client (push) Successful in 25s
OfficeCom Sentinel Client / build-client-windows (push) Has been cancelled
2026-07-25 01:31:00 +02:00
OfficeCom Codex
1ad720f919 Simplify runs-on labels for Gitea Actions
Some checks failed
OfficeCom Sentinel Client / validate-client (push) Successful in 1m11s
OfficeCom Sentinel Client / build-client-windows (push) Has been cancelled
2026-07-17 02:07:29 +02:00
OfficeCom Codex
61c5e7823a Split Linux validation and Windows release build
Some checks failed
OfficeCom Sentinel Client / validate-client (push) Has been cancelled
OfficeCom Sentinel Client / build-client-windows (push) Has been cancelled
2026-07-17 02:03:43 +02:00
OfficeCom Codex
05029c9fb2 Track internal Postgres target in release process
Some checks failed
OfficeCom Sentinel Client / build-client (push) Has been cancelled
2026-07-17 01:12:38 +02:00
OfficeCom Codex
fde24f2616 Add standalone OCSentinel setup executable builder
Some checks failed
OfficeCom Sentinel Client / build-client (push) Has been cancelled
2026-07-17 01:07:37 +02:00
OfficeCom Codex
38a99f2706 Add stable manifest URL for NinjaOne deployments
Some checks failed
OfficeCom Sentinel Client / build-client (push) Has been cancelled
2026-07-17 01:05:56 +02:00
OfficeCom Codex
e8b7a2831d Document manual NinjaOne rollout for v1.2.3
Some checks failed
OfficeCom Sentinel Client / build-client (push) Has been cancelled
2026-07-17 01:00:34 +02:00
26 changed files with 878 additions and 15 deletions

View File

@@ -7,12 +7,35 @@ on:
tags:
- "v*"
workflow_dispatch:
inputs:
build_windows:
description: "Build the Windows release package"
required: false
default: "false"
jobs:
build-client:
runs-on:
- self-hosted
- windows
validate-client:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: "10.0.x"
- name: Restore client
run: dotnet restore ./src/OCSentinelCli/OCSentinelCli.csproj
- name: Build client
run: dotnet build ./src/OCSentinelCli/OCSentinelCli.csproj -c Release --no-restore
build-client-windows:
needs: validate-client
# .NET can publish a self-contained Windows x64 client from Linux.
# This keeps releases independent of a Windows Gitea runner.
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -28,10 +51,14 @@ jobs:
./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 }}"
$ref = if ($env:GITHUB_REF) { $env:GITHUB_REF } else { $env:GITEA_REF }
$tag = if ($env:GITHUB_REF_NAME) { $env:GITHUB_REF_NAME } else { Split-Path -Leaf $ref }
if ($tag -notlike "v*") {
Write-Host "Not a version tag; skipping release manifest."
exit 0
}
$artifactUrl = "https://gitea.officecom.cloud/officecom/oc-sentinel/releases/download/$tag/OCSentinelClient-win-x64.zip"
./build/build-release-manifest.ps1 -ArtifactUrl $artifactUrl
@@ -44,3 +71,41 @@ jobs:
artifacts/OCSentinelClient-win-x64.zip.sha256
artifacts/version.json
if-no-files-found: warn
- name: Publish Gitea release assets
shell: pwsh
env:
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
$ref = if ($env:GITHUB_REF) { $env:GITHUB_REF } else { $env:GITEA_REF }
$tag = if ($env:GITHUB_REF_NAME) { $env:GITHUB_REF_NAME } else { Split-Path -Leaf $ref }
if ($tag -notlike "v*") {
Write-Host "Not a version tag; skipping Gitea release publication."
exit 0
}
$headers = @{ Authorization = "token $env:GITEA_TOKEN" }
$repository = if ($env:GITEA_REPOSITORY) { $env:GITEA_REPOSITORY } else { $env:GITHUB_REPOSITORY }
$serverUrl = if ($env:GITEA_SERVER_URL) { $env:GITEA_SERVER_URL } else { $env:GITHUB_SERVER_URL }
if ([string]::IsNullOrWhiteSpace($env:GITEA_TOKEN) -or [string]::IsNullOrWhiteSpace($repository) -or [string]::IsNullOrWhiteSpace($serverUrl)) {
throw "Gitea release environment is incomplete. Expected GITEA_TOKEN, repository, and server URL."
}
$baseUrl = "$serverUrl/api/v1/repos/$repository"
$releaseBody = @{
tag_name = $tag
target_commitish = "${{ github.sha }}"
name = "OfficeCom Sentinel $tag"
body = "Automated OfficeCom Sentinel client release."
} | ConvertTo-Json
try {
$release = Invoke-RestMethod -Method Get -Headers $headers -Uri "$baseUrl/releases/tags/$tag"
}
catch {
$release = Invoke-RestMethod -Method Post -Headers $headers -ContentType "application/json" -Body $releaseBody -Uri "$baseUrl/releases"
}
foreach ($file in @("artifacts/OCSentinelClient-win-x64.zip", "artifacts/OCSentinelClient-win-x64.zip.sha256", "artifacts/version.json")) {
$assetName = [System.IO.Path]::GetFileName($file)
Invoke-RestMethod -Method Post -Headers $headers -InFile $file -ContentType "application/octet-stream" -Uri "$baseUrl/releases/$($release.id)/assets?name=$assetName" | Out-Null
}

1
.gitignore vendored
View File

@@ -10,6 +10,7 @@ payload/
*.log
*.zip
*.sha256
payload.zip
SetupAttackTracer.exe
decompiled/
msi-admin/

View File

@@ -9,7 +9,10 @@ OfficeCom Sentinel is the hardened endpoint client for Windows event correlation
- Ninja monitor wrapper: `scripts/run-ocsentinel-monitor.ps1`
- packaged installer runtime: `installer/runtime-run-ocsentinel.ps1`
- package builder: `build/build-client-package.ps1`
- setup EXE builder: `build/build-client-installer.ps1`
- update manifest builder: `build/build-release-manifest.ps1`
- release checklist: `docs/release-checklist.md`
- internal server-side target example: `infra/postgres-target.example.json`
## Build

View File

@@ -0,0 +1,50 @@
param(
[string]$Configuration = "Release"
)
$ErrorActionPreference = "Stop"
$repoRoot = Split-Path -Parent $PSScriptRoot
$artifactsRoot = Join-Path $repoRoot "artifacts"
$zipPath = Join-Path $artifactsRoot "OCSentinelClient-win-x64.zip"
$bootstrapperRoot = Join-Path $repoRoot "installer\OCSentinelBootstrapper"
$payloadPath = Join-Path $bootstrapperRoot "payload.zip"
$projectPath = Join-Path $bootstrapperRoot "OCSentinelBootstrapper.csproj"
$publishRoot = Join-Path $artifactsRoot "bootstrapper-publish\win-x64"
$outputExe = Join-Path $artifactsRoot "OCSentinelSetup.exe"
if (-not (Test-Path $zipPath)) {
& powershell.exe -ExecutionPolicy Bypass -File (Join-Path $repoRoot "build\build-client-package.ps1") -Configuration $Configuration
if ($LASTEXITCODE -ne 0) {
throw "build-client-package.ps1 failed"
}
}
Copy-Item -Path $zipPath -Destination $payloadPath -Force
if (Test-Path $publishRoot) {
Remove-Item -LiteralPath $publishRoot -Recurse -Force
}
if (Test-Path $outputExe) {
Remove-Item -LiteralPath $outputExe -Force
}
New-Item -ItemType Directory -Force -Path $publishRoot | Out-Null
& dotnet publish $projectPath `
-c $Configuration `
-r win-x64 `
--self-contained true `
-p:PublishSingleFile=true `
-p:EnableCompressionInSingleFile=true `
-p:IncludeNativeLibrariesForSelfExtract=true `
-o $publishRoot
if ($LASTEXITCODE -ne 0) {
throw "dotnet publish failed for bootstrapper"
}
Copy-Item -Path (Join-Path $publishRoot "OCSentinelBootstrapper.exe") -Destination $outputExe -Force
Write-Host "OfficeCom Sentinel setup executable created at $outputExe"

View File

@@ -58,6 +58,7 @@ Copy-Item -Path (Join-Path $repoRoot "scripts\protect-ocsentinel-secret.ps1") -D
Copy-Item -Path (Join-Path $repoRoot "config\ocsentinel-settings.example.json") -Destination (Join-Path $packageRoot "config\ocsentinel-settings.example.json") -Force
Copy-Item -Path (Join-Path $repoRoot "config\ocsentinel-client.example.json") -Destination (Join-Path $packageRoot "config\ocsentinel-client.example.json") -Force
Copy-Item -Path (Join-Path $repoRoot "config\ocsentinel-client.dev.example.json") -Destination (Join-Path $packageRoot "config\ocsentinel-client.dev.example.json") -Force
Copy-Item -Path (Join-Path $repoRoot "config\update-channel.example.json") -Destination (Join-Path $packageRoot "config\update-channel.example.json") -Force
Copy-Item -Path (Join-Path $repoRoot "samples\ninja-vulnerability-export.example.csv") -Destination (Join-Path $packageRoot "samples\ninja-vulnerability-export.example.csv") -Force
Copy-Item -Path (Join-Path $repoRoot "samples\webhook-payload.example.json") -Destination (Join-Path $packageRoot "samples\webhook-payload.example.json") -Force

View File

@@ -0,0 +1,12 @@
{
"schemaVersion": "2.0",
"environment": "development",
"lookbackDays": 7,
"topFindings": 10,
"n8nWebhookUrl": "http://172.16.41.197:5678/webhook/ocsentinel-ingest",
"deviceIdentifierMode": "machineName",
"uploadTimeoutSeconds": 30,
"enableVulnerabilityCorrelation": true,
"vulnerabilityCsvPath": "",
"secretReference": "device-default"
}

View File

@@ -19,6 +19,15 @@ The repository now keeps only the client-side architecture:
- optional n8n upload
- packaged ZIP release flow for NinjaOne deployment
The client must not depend on a PostgreSQL IP or hostname. PostgreSQL stays a server-side concern behind the ingest or n8n layer.
## PostgreSQL Handling
- PostgreSQL is not contacted directly by endpoint clients.
- The PostgreSQL host or IP should be tracked in the repository only as internal deployment metadata.
- Review that internal target on every release before publishing.
- Keep the actual production value in a private operational copy if it should not be visible in the public repository.
## Removed Model
The following older pieces are intentionally no longer part of the repo:

View File

@@ -17,7 +17,7 @@ Build these locally with:
```powershell
powershell -ExecutionPolicy Bypass -File .\build\build-client-package.ps1
powershell -ExecutionPolicy Bypass -File .\build\build-release-manifest.ps1 `
-ArtifactUrl "https://gitea.officecom.cloud/officecom/oc-sentinel/releases/download/v2.0.0/OCSentinelClient-win-x64.zip"
-ArtifactUrl "https://gitea.officecom.cloud/officecom/oc-sentinel/releases/download/v1.2.3/OCSentinelClient-win-x64.zip"
```
## Installed Layout
@@ -32,11 +32,20 @@ powershell -ExecutionPolicy Bypass -File .\build\build-release-manifest.ps1 `
## NinjaOne Tasks
Initial install/update:
Create a PowerShell script in NinjaOne named `OCSentinel - Installieren oder aktualisieren`.
Run it as `SYSTEM` in 64-bit PowerShell and copy the content of
`scripts/bootstrap-ocsentinel-ninja.ps1` into the NinjaOne script editor.
It is idempotent: new devices install the current package, while installed devices
only update when a newer manifest version is published.
Use it for the one-time rollout and, later, as the monthly update task. For an
initial validation scan, add `-RunInitialStatusScan` to the script parameters.
Installed-client update only:
```powershell
& "C:\Program Files\OCSentinel\scripts\update-ocsentinel.ps1" `
-ManifestUrl "https://gitea.officecom.cloud/officecom/oc-sentinel/releases/download/v2.0.0/version.json" `
-ManifestUrl "https://gitea.officecom.cloud/officecom/oc-sentinel/raw/main/release/stable/version.json" `
-Force
```
@@ -44,7 +53,7 @@ Routine update:
```powershell
& "C:\Program Files\OCSentinel\scripts\update-ocsentinel.ps1" `
-ManifestUrl "https://gitea.officecom.cloud/officecom/oc-sentinel/releases/download/v2.0.0/version.json"
-ManifestUrl "https://gitea.officecom.cloud/officecom/oc-sentinel/raw/main/release/stable/version.json"
```
Runtime:
@@ -65,3 +74,31 @@ Runtime:
This writes:
- `C:\ProgramData\OCSentinel\secrets\ocsentinel-upload-secret.dat`
## Development Upload
For the internal development environment, copy
`config/ocsentinel-client.dev.example.json` to the installed client config
path and use its HTTP webhook URL. Production clients must use the HTTPS
configuration with the public Sentinel domain instead.
## Current Manual Release State
As of July 16, 2026, the first manual release is already published:
- tag: `v1.2.3`
- release URL: `https://gitea.officecom.cloud/officecom/oc-sentinel/releases/tag/v1.2.3`
- stable manifest URL: `https://gitea.officecom.cloud/officecom/oc-sentinel/raw/main/release/stable/version.json`
The manifest is intentionally version-independent for NinjaOne. Only the JSON content changes per release; the NinjaOne task URL stays the same.
This means NinjaOne rollout can start immediately without waiting for a Gitea runner.
## Later Automation
When a Gitea runner is added later, the usual next step is:
1. connect to the runner host through SSH or RDP, depending on the server type
2. install and register the Gitea runner
3. let `.gitea/workflows/client-build.yml` publish future release artifacts automatically
4. update `release/stable/version.json` automatically as part of the release flow

View File

@@ -25,3 +25,46 @@ n8n is responsible for:
- storage in the central backend
- organization-wide aggregation
- NinjaOne organization API updates
## Required n8n Workflow
The webhook itself may be reachable only on the internal network. It does not
require public access to the n8n editor or API. Every managed device must be
able to reach the webhook URL over HTTPS.
For the isolated development environment only, HTTP is permitted at
`http://172.16.41.197:5678/webhook/ocsentinel-ingest`. Do not reuse this URL,
the development shared secret, or a disabled-TLS configuration in production.
1. `Webhook`: accept `POST` on the configured private URL.
2. `Code`: reject a request if `X-ATN-Device`, `X-ATN-Timestamp`,
`X-ATN-Nonce`, `X-ATN-Version`, `X-ATN-Payload-SHA256`, or
`X-ATN-Signature` is missing; reject timestamps outside five minutes.
3. `Code`: calculate SHA-256 over the raw request body and compare it with
`X-ATN-Payload-SHA256`. Calculate HMAC-SHA256 over the following exact
newline-separated string and compare it in constant time with
`X-ATN-Signature`:
```text
<device>\n<timestamp>\n<nonce>\n<version>\n<payload-sha256>
```
4. `Postgres`: insert the nonce into `ocsentinel.ingest_nonce` with a short
expiry. If it already exists, return `409` and do not process the report.
5. `Postgres`: upsert the device, insert a row in `ocsentinel.scan_report`,
then return `202`.
6. A separate scheduled n8n workflow reads
`ocsentinel.organization_summary` and `ocsentinel.current_device_status`
to update the NinjaOne organization fields through the API.
Use an n8n credential for the shared HMAC secret and a separate n8n credential
for PostgreSQL. Do not store either value in workflow JSON or this repository.
For the current Docker deployment, use the private hostname
`ocsentinel-postgres` and the restricted database role `ocsentinel_n8n`; see
`infra/dockge/README.md` for the remaining credential fields.
## PostgreSQL Scope
- The client only knows its outward upload destination.
- PostgreSQL connection details belong to the internal ingest or n8n side.
- If the PostgreSQL IP changes, update the internal server-side configuration and review it during the next release.

25
docs/release-checklist.md Normal file
View File

@@ -0,0 +1,25 @@
# Release Checklist
## Goal
Use this checklist before publishing every OfficeCom Sentinel release.
## Infrastructure Check
1. Confirm the internal PostgreSQL target is still correct in `infra/postgres-target.example.json` or its private production counterpart.
2. Confirm the n8n internal base URL is still correct.
3. Confirm the public client ingest URL still forwards to the intended internal service.
4. Confirm no internal PostgreSQL host or IP is embedded in client configuration, installer output, or public release artifacts.
## Build Check
1. Build `OCSentinelClient-win-x64.zip`.
2. Build `OCSentinelSetup.exe`.
3. Generate `version.json`.
4. Verify SHA-256 output matches the released ZIP.
## Publish Check
1. Upload the ZIP, SHA256 file, and setup EXE to the release.
2. Update `release/stable/version.json` so NinjaOne keeps a version-independent manifest URL.
3. If infrastructure changed, update the internal Postgres target record in the repo at the same time.

44
infra/dockge/README.md Normal file
View File

@@ -0,0 +1,44 @@
# OCSentinel PostgreSQL Dockge Stack
Production is deployed as the Dockge stack directory:
```text
/dockerstacks/ocsentinel-postgres
```
The stack runs `postgres:16-alpine` as `ocsentinel-postgres` and joins the
existing Docker network `n8n_n8n-network`. It deliberately has no `ports:`
mapping, so PostgreSQL is not exposed on the host network or the Internet.
The stack owns these private files on the server:
```text
/dockerstacks/ocsentinel-postgres/compose.yaml
/dockerstacks/ocsentinel-postgres/.env
/dockerstacks/ocsentinel-postgres/init/
/dockerstacks/ocsentinel-postgres/data/
```
`.env` is root-readable only and contains both the PostgreSQL administrator
password and the restricted `ocsentinel_n8n` password. Never commit it or copy
it to endpoint devices.
## n8n PostgreSQL Credential
Create one credential in n8n with these non-secret values:
| Setting | Value |
| --- | --- |
| Host | `ocsentinel-postgres` |
| Port | `5432` |
| Database | `ocsentinel` |
| User | `ocsentinel_n8n` |
| SSL | disabled (private Docker network) |
Retrieve the password only on the server when entering the n8n credential:
```bash
sudo grep '^OCSENTINEL_N8N_PASSWORD=' /dockerstacks/ocsentinel-postgres/.env
```
The database schema source remains [../postgres/001_ocsentinel.sql](../postgres/001_ocsentinel.sql).

View File

@@ -0,0 +1,10 @@
{
"role": "server-side-only",
"environment": "production",
"postgresHost": "10.0.0.25",
"postgresPort": 5432,
"postgresDatabase": "ocsentinel",
"postgresSslMode": "require",
"n8nInternalBaseUrl": "http://n8n.internal:5678",
"notes": "This file is for the internal ingest or n8n side only. Do not deploy this file to endpoint clients."
}

View File

@@ -0,0 +1,81 @@
-- OfficeCom Sentinel central reporting store.
-- Apply once as a PostgreSQL administrator to the dedicated ocsentinel database.
BEGIN;
CREATE SCHEMA IF NOT EXISTS ocsentinel;
CREATE TABLE IF NOT EXISTS ocsentinel.device (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
machine_name TEXT NOT NULL,
machine_name_key TEXT NOT NULL UNIQUE,
first_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(),
last_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(),
last_client_version TEXT NOT NULL DEFAULT ''
);
CREATE TABLE IF NOT EXISTS ocsentinel.scan_report (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
device_id BIGINT NOT NULL REFERENCES ocsentinel.device(id) ON DELETE CASCADE,
generated_at_utc TIMESTAMPTZ NOT NULL,
received_at TIMESTAMPTZ NOT NULL DEFAULT now(),
client_version TEXT NOT NULL,
alert_state TEXT NOT NULL CHECK (alert_state IN ('ok', 'warning', 'critical', 'unknown')),
base_alert_state TEXT NOT NULL CHECK (base_alert_state IN ('ok', 'warning', 'critical', 'unknown')),
total_events INTEGER NOT NULL CHECK (total_events >= 0),
unique_ip_count INTEGER NOT NULL CHECK (unique_ip_count >= 0),
cve_total INTEGER NOT NULL DEFAULT 0 CHECK (cve_total >= 0),
cve_critical INTEGER NOT NULL DEFAULT 0 CHECK (cve_critical >= 0),
payload_sha256 CHAR(64) NOT NULL,
payload JSONB NOT NULL,
UNIQUE (device_id, generated_at_utc, payload_sha256)
);
CREATE INDEX IF NOT EXISTS ix_ocsentinel_scan_report_device_received
ON ocsentinel.scan_report (device_id, received_at DESC);
CREATE INDEX IF NOT EXISTS ix_ocsentinel_scan_report_alert_received
ON ocsentinel.scan_report (alert_state, received_at DESC);
CREATE TABLE IF NOT EXISTS ocsentinel.ingest_nonce (
nonce CHAR(32) PRIMARY KEY,
device_id BIGINT NOT NULL REFERENCES ocsentinel.device(id) ON DELETE CASCADE,
received_at TIMESTAMPTZ NOT NULL DEFAULT now(),
expires_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS ix_ocsentinel_ingest_nonce_expires
ON ocsentinel.ingest_nonce (expires_at);
CREATE OR REPLACE VIEW ocsentinel.current_device_status AS
SELECT DISTINCT ON (d.id)
d.machine_name,
d.first_seen_at,
d.last_seen_at,
d.last_client_version,
r.generated_at_utc,
r.received_at,
r.alert_state,
r.base_alert_state,
r.total_events,
r.unique_ip_count,
r.cve_total,
r.cve_critical,
r.payload
FROM ocsentinel.device AS d
LEFT JOIN ocsentinel.scan_report AS r ON r.device_id = d.id
ORDER BY d.id, r.generated_at_utc DESC NULLS LAST, r.received_at DESC NULLS LAST;
CREATE OR REPLACE VIEW ocsentinel.organization_summary AS
SELECT
count(*) FILTER (WHERE generated_at_utc IS NOT NULL) AS devices_reporting,
count(*) FILTER (WHERE alert_state = 'warning') AS devices_warning,
count(*) FILTER (WHERE alert_state = 'critical') AS devices_critical,
coalesce(sum(total_events), 0) AS total_events,
coalesce(sum(unique_ip_count), 0) AS total_unique_ips,
coalesce(sum(cve_total), 0) AS total_cves,
coalesce(sum(cve_critical), 0) AS critical_cves,
max(received_at) AS last_report_received_at
FROM ocsentinel.current_device_status;
COMMIT;

36
infra/postgres/README.md Normal file
View File

@@ -0,0 +1,36 @@
# OfficeCom Sentinel PostgreSQL
PostgreSQL is the private central store for endpoint reports. It is never
contacted directly by an endpoint; only n8n uses a database account.
## Provisioning
The production instance is deployed as the private Dockge stack documented in
[../dockge/README.md](../dockge/README.md). The bootstrap has already created
the database, schema, and restricted `ocsentinel_n8n` role.
For a separate future installation:
1. Create a database named `ocsentinel` on the private PostgreSQL server.
2. Apply `001_ocsentinel.sql` as a database administrator.
3. Create a non-superuser n8n login and grant only the necessary permissions:
```sql
GRANT USAGE ON SCHEMA ocsentinel TO ocsentinel_n8n;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA ocsentinel TO ocsentinel_n8n;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA ocsentinel TO ocsentinel_n8n;
GRANT SELECT ON ocsentinel.current_device_status, ocsentinel.organization_summary TO ocsentinel_n8n;
ALTER DEFAULT PRIVILEGES IN SCHEMA ocsentinel
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO ocsentinel_n8n;
```
Keep the database host, password, and TLS settings only in n8n credentials.
They do not belong in Gitea, NinjaOne scripts, or endpoint configuration.
## Maintenance
Run monthly from n8n or an administrator session to remove expired replay tokens:
```sql
DELETE FROM ocsentinel.ingest_nonce WHERE expires_at < now();
```

View File

@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UseWindowsForms>false</UseWindowsForms>
<AssemblyName>OCSentinelBootstrapper</AssemblyName>
<RootNamespace>OCSentinelBootstrapper</RootNamespace>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="payload.zip" LogicalName="payload.zip" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,76 @@
using System.Diagnostics;
using System.IO.Compression;
using System.Reflection;
namespace OCSentinelBootstrapper;
internal static class Program
{
private static int Main()
{
string tempRoot = Path.Combine(Path.GetTempPath(), "OCSentinelSetup", Guid.NewGuid().ToString("N"));
string zipPath = Path.Combine(tempRoot, "payload.zip");
string extractRoot = Path.Combine(tempRoot, "payload");
try
{
Directory.CreateDirectory(tempRoot);
Directory.CreateDirectory(extractRoot);
ExtractEmbeddedPayload(zipPath);
ZipFile.ExtractToDirectory(zipPath, extractRoot, overwriteFiles: true);
string installScript = Path.Combine(extractRoot, "scripts", "install-ocsentinel.ps1");
if (!File.Exists(installScript))
{
throw new FileNotFoundException("Embedded payload did not contain install-ocsentinel.ps1", installScript);
}
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "powershell.exe",
Arguments = $"-ExecutionPolicy Bypass -File \"{installScript}\"",
UseShellExecute = false
}
};
process.Start();
process.WaitForExit();
return process.ExitCode;
}
catch (Exception ex)
{
Console.Error.WriteLine($"OCSentinel installer failed: {ex}");
return 1;
}
finally
{
try
{
if (Directory.Exists(tempRoot))
{
Directory.Delete(tempRoot, recursive: true);
}
}
catch
{
// Best-effort cleanup only.
}
}
}
private static void ExtractEmbeddedPayload(string destinationPath)
{
Assembly assembly = Assembly.GetExecutingAssembly();
using Stream? resourceStream = assembly.GetManifestResourceStream("payload.zip");
if (resourceStream is null)
{
throw new InvalidOperationException("Embedded payload.zip resource was not found.");
}
using FileStream output = File.Create(destinationPath);
resourceStream.CopyTo(output);
}
}

View File

@@ -26,6 +26,9 @@ Copy-Item -Path (Join-Path $packageRoot "config\ocsentinel-settings.example.json
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
}
if (Test-Path (Join-Path $packageRoot "config\ocsentinel-client.dev.example.json")) {
Copy-Item -Path (Join-Path $packageRoot "config\ocsentinel-client.dev.example.json") -Destination (Join-Path $configRoot "ocsentinel-client.dev.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

View File

@@ -3,6 +3,10 @@ param(
[int]$TopCount = 10,
[string]$OutputPath = "..\reports\ocsentinel-summary.json",
[string]$ConfigPath = "..\config\ocsentinel-settings.json",
[string]$ClientConfigPath = "..\config\ocsentinel-client.json",
[string]$SecretPath = "",
[ValidateSet("disabled", "auto", "required")]
[string]$UploadMode = "auto",
[string]$VulnerabilityCsvPath = "",
[string]$MirrorRoot = "",
[ValidateSet("status", "attack-only", "cve-critical", "attack-plus-cve")]
@@ -142,9 +146,15 @@ $runnerArgs = @(
"-LookbackDays", $LookbackDays,
"-TopCount", $TopCount,
"-OutputPath", $OutputPath,
"-ConfigPath", $ConfigPath
"-ConfigPath", $ConfigPath,
"-ClientConfigPath", $ClientConfigPath,
"-UploadMode", $UploadMode
)
if (-not [string]::IsNullOrWhiteSpace($SecretPath)) {
$runnerArgs += @("-SecretPath", $SecretPath)
}
if (-not [string]::IsNullOrWhiteSpace($VulnerabilityCsvPath)) {
$runnerArgs += @("-VulnerabilityCsvPath", $VulnerabilityCsvPath)
}

View File

@@ -0,0 +1,8 @@
{
"channel": "stable",
"version": "1.2.6",
"publishedAtUtc": "2026-07-25T00:00:00Z",
"artifactUrl": "https://gitea.officecom.cloud/officecom/oc-sentinel/releases/download/v1.2.6/OCSentinelClient-win-x64.zip",
"sha256": "d6256f3e376701bff18d9dfd8d5825498e85fb6d94c15227828a50678693d097",
"minUpdaterVersion": "1.0.0"
}

View File

@@ -0,0 +1,128 @@
[CmdletBinding()]
param(
[string]$ManifestUrl = "https://gitea.officecom.cloud/officecom/oc-sentinel/raw/main/release/stable/version.json",
[string]$WebhookUrl = "",
[string]$SecretValue = "",
[switch]$RunInitialStatusScan
)
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$installRoot = Join-Path $env:ProgramFiles "OCSentinel"
$updaterPath = Join-Path $installRoot "scripts\update-ocsentinel.ps1"
$monitorPath = Join-Path $installRoot "scripts\run-ocsentinel-monitor.ps1"
$appPath = Join-Path $installRoot "app\OCSentinelCli.exe"
$clientConfigPath = Join-Path $installRoot "config\ocsentinel-client.json"
$secretScriptPath = Join-Path $installRoot "scripts\protect-ocsentinel-secret.ps1"
$secretPath = "C:\ProgramData\OCSentinel\secrets\ocsentinel-upload-secret.dat"
function Assert-ArtifactSignature {
param([Parameter(Mandatory)][string]$ExecutablePath)
$signature = Get-AuthenticodeSignature -FilePath $ExecutablePath
if ($signature.Status -notin @("Valid", "NotSigned")) {
throw "Executable signature validation failed with status: $($signature.Status)"
}
if ($signature.Status -eq "NotSigned") {
Write-Warning "The package hash was verified, but OCSentinelCli.exe is not code-signed yet."
}
}
if (Test-Path -LiteralPath $updaterPath) {
Write-Host "Existing OCSentinel installation found. Checking for updates."
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $updaterPath -ManifestUrl $ManifestUrl
if ($LASTEXITCODE -ne 0) {
throw "OCSentinel updater exited with code $LASTEXITCODE"
}
}
else {
Write-Host "Reading OCSentinel release manifest: $ManifestUrl"
$manifest = Invoke-RestMethod -Method Get -Uri $ManifestUrl -TimeoutSec 60
if ([string]::IsNullOrWhiteSpace($manifest.version) -or [string]::IsNullOrWhiteSpace($manifest.artifactUrl) -or [string]::IsNullOrWhiteSpace($manifest.sha256)) {
throw "Release manifest is missing version, artifactUrl, or sha256."
}
$downloadRoot = Join-Path $env:ProgramData ("OCSentinel\\bootstrap\\" + [Guid]::NewGuid().ToString("N"))
$zipPath = Join-Path $downloadRoot "OCSentinelClient.zip"
$extractRoot = Join-Path $downloadRoot "payload"
try {
New-Item -ItemType Directory -Force -Path $extractRoot | Out-Null
Write-Host "Downloading OCSentinel $($manifest.version)"
Invoke-WebRequest -Uri ([string]$manifest.artifactUrl) -OutFile $zipPath -TimeoutSec 300
$actualHash = (Get-FileHash -LiteralPath $zipPath -Algorithm SHA256).Hash.ToLowerInvariant()
$expectedHash = ([string]$manifest.sha256).ToLowerInvariant()
if ($actualHash -ne $expectedHash) {
throw "SHA-256 mismatch for the downloaded OCSentinel package."
}
Write-Host "Package hash verified. Extracting release payload."
Expand-Archive -LiteralPath $zipPath -DestinationPath $extractRoot -Force
$payloadApp = Get-ChildItem -Path $extractRoot -Recurse -Filter "OCSentinelCli.exe" | Select-Object -First 1
$installer = Get-ChildItem -Path $extractRoot -Recurse -Filter "install-ocsentinel.ps1" | Select-Object -First 1
if ($null -eq $payloadApp -or $null -eq $installer) {
throw "The downloaded package is incomplete."
}
Assert-ArtifactSignature -ExecutablePath $payloadApp.FullName
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $installer.FullName
if ($LASTEXITCODE -ne 0) {
throw "OCSentinel installer exited with code $LASTEXITCODE"
}
}
finally {
if (Test-Path -LiteralPath $downloadRoot) {
Remove-Item -LiteralPath $downloadRoot -Recurse -Force
}
}
}
if (-not (Test-Path -LiteralPath $appPath)) {
throw "OCSentinel installation completed, but the client executable was not found."
}
if (-not [string]::IsNullOrWhiteSpace($WebhookUrl)) {
if (-not (Test-Path -LiteralPath $clientConfigPath)) {
throw "OCSentinel client configuration was not found: $clientConfigPath"
}
$clientConfig = Get-Content -LiteralPath $clientConfigPath -Raw | ConvertFrom-Json
$clientConfig.n8nWebhookUrl = $WebhookUrl
$clientConfig.environment = "production"
$clientConfig | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $clientConfigPath -Encoding UTF8
Write-Host "Configured OCSentinel upload endpoint."
}
if (-not [string]::IsNullOrWhiteSpace($SecretValue)) {
if (-not (Test-Path -LiteralPath $secretScriptPath)) {
throw "OCSentinel secret bootstrap script was not found: $secretScriptPath"
}
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $secretScriptPath -SecretValue $SecretValue
if ($LASTEXITCODE -ne 0) {
throw "OCSentinel secret bootstrap failed with code $LASTEXITCODE"
}
}
if ($RunInitialStatusScan) {
if (-not (Test-Path -LiteralPath $monitorPath)) {
throw "OCSentinel was installed, but the monitor script is missing."
}
Write-Host "Running initial OCSentinel status scan."
$scanArguments = @("-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $monitorPath, "-Mode", "status", "-OutputPath", "..\\reports\\ocsentinel-summary.json")
if ((Test-Path -LiteralPath $clientConfigPath) -and (Test-Path -LiteralPath $secretPath)) {
$scanArguments += @("-ClientConfigPath", $clientConfigPath, "-SecretPath", $secretPath, "-UploadMode", "required")
}
& powershell.exe @scanArguments
if ($LASTEXITCODE -ne 0) {
throw "Initial OCSentinel status scan exited with code $LASTEXITCODE"
}
}
Write-Host "OCSentinel bootstrap completed successfully."

View File

@@ -3,6 +3,10 @@ param(
[int]$TopCount = 10,
[string]$OutputPath = ".\reports\ocsentinel-summary.json",
[string]$ConfigPath = ".\config\ocsentinel-settings.example.json",
[string]$ClientConfigPath = ".\config\ocsentinel-client.json",
[string]$SecretPath = "",
[ValidateSet("disabled", "auto", "required")]
[string]$UploadMode = "auto",
[string]$VulnerabilityCsvPath = "",
[string]$MirrorRoot = "",
[ValidateSet("status", "attack-only", "cve-critical", "attack-plus-cve")]
@@ -142,9 +146,15 @@ $runnerArgs = @(
"-LookbackDays", $LookbackDays,
"-TopCount", $TopCount,
"-OutputPath", $OutputPath,
"-ConfigPath", $ConfigPath
"-ConfigPath", $ConfigPath,
"-ClientConfigPath", $ClientConfigPath,
"-UploadMode", $UploadMode
)
if (-not [string]::IsNullOrWhiteSpace($SecretPath)) {
$runnerArgs += @("-SecretPath", $SecretPath)
}
if (-not [string]::IsNullOrWhiteSpace($VulnerabilityCsvPath)) {
$runnerArgs += @("-VulnerabilityCsvPath", $VulnerabilityCsvPath)
}

View File

@@ -0,0 +1,124 @@
[CmdletBinding()]
param(
[string]$RegistrationToken,
[string]$InstanceUrl = "https://gitea.officecom.cloud",
[string]$RunnerName = "officecom-oc-sentinel-windows-01",
[string]$RunnerVersion = "1.0.8",
[string]$RunnerAccount = "OCGiteaRunner",
[string]$InstallRoot = "$env:ProgramData\\OCGiteaRunner"
)
$ErrorActionPreference = "Stop"
function Test-IsAdministrator {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal($identity)
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
function New-RunnerPassword {
# The account is only used by Task Scheduler; no password is persisted in this script or repository.
$bytes = New-Object byte[] 36
[Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes)
return [Convert]::ToBase64String($bytes).Replace('+', 'A').Replace('/', 'B').Replace('=', 'C') + "!9z"
}
if (-not (Test-IsAdministrator)) {
throw "Run this script from an elevated PowerShell window (Run as administrator)."
}
if ([string]::IsNullOrWhiteSpace($RegistrationToken)) {
$secureToken = Read-Host "Paste the repository runner registration token" -AsSecureString
$tokenPointer = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureToken)
try {
$RegistrationToken = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($tokenPointer)
}
finally {
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($tokenPointer)
}
}
if ([string]::IsNullOrWhiteSpace($RegistrationToken)) {
throw "A repository runner registration token is required."
}
$taskName = "OCSentinel Gitea Windows Runner"
$runnerPath = Join-Path $InstallRoot "gitea-runner.exe"
$configPath = Join-Path $InstallRoot "config.yaml"
$runnerStatePath = Join-Path $InstallRoot ".runner"
$logDirectory = Join-Path $InstallRoot "logs"
$downloadUrl = "https://gitea.com/gitea/act_runner/releases/download/v$RunnerVersion/gitea-runner-$RunnerVersion-windows-amd64.exe"
$checksumUrl = "$downloadUrl.sha256"
$accountQualifiedName = "$env:COMPUTERNAME\\$RunnerAccount"
if (Test-Path -LiteralPath $runnerStatePath) {
throw "A runner is already registered at $InstallRoot. Remove it in Gitea first, then remove this directory if a new registration is needed."
}
$existingAccount = Get-LocalUser -Name $RunnerAccount -ErrorAction SilentlyContinue
if ($existingAccount) {
throw "The local account '$RunnerAccount' already exists. Stop and remove the existing runner before reinstalling it."
}
New-Item -ItemType Directory -Force -Path $InstallRoot, $logDirectory | Out-Null
try {
Write-Host "Downloading Gitea runner $RunnerVersion..."
Invoke-WebRequest -UseBasicParsing -Uri $downloadUrl -OutFile $runnerPath
$checksumText = (Invoke-WebRequest -UseBasicParsing -Uri $checksumUrl).Content.Trim()
$expectedHash = ($checksumText -split '\s+')[0].ToLowerInvariant()
$actualHash = (Get-FileHash -LiteralPath $runnerPath -Algorithm SHA256).Hash.ToLowerInvariant()
if ($actualHash -ne $expectedHash) {
throw "Runner checksum verification failed."
}
$password = New-RunnerPassword
$securePassword = ConvertTo-SecureString -String $password -AsPlainText -Force
New-LocalUser -Name $RunnerAccount -Password $securePassword -Description "Restricted account for the OCSentinel Gitea Actions runner." -AccountNeverExpires | Out-Null
$config = & $runnerPath generate-config
$config = $config -replace '(?m)^ labels:.*$', ' labels: ["windows:host"]'
Set-Content -LiteralPath $configPath -Value $config -Encoding utf8
# Build jobs run only with this non-administrative account and only for the repository runner token supplied.
$acl = Get-Acl -LiteralPath $InstallRoot
$acl.SetAccessRuleProtection($true, $false)
$systemRule = New-Object Security.AccessControl.FileSystemAccessRule("SYSTEM", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow")
$adminRule = New-Object Security.AccessControl.FileSystemAccessRule("BUILTIN\\Administrators", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow")
$runnerRule = New-Object Security.AccessControl.FileSystemAccessRule($accountQualifiedName, "Modify", "ContainerInherit,ObjectInherit", "None", "Allow")
$acl.AddAccessRule($systemRule)
$acl.AddAccessRule($adminRule)
$acl.AddAccessRule($runnerRule)
Set-Acl -LiteralPath $InstallRoot -AclObject $acl
$credential = New-Object Management.Automation.PSCredential($accountQualifiedName, $securePassword)
$registerArgs = @(
"--config", "`"$configPath`"", "register", "--no-interactive",
"--instance", "`"$InstanceUrl`"", "--token", "`"$RegistrationToken`"",
"--name", "`"$RunnerName`"", "--labels", "windows:host"
) -join " "
$registration = Start-Process -FilePath $runnerPath -ArgumentList $registerArgs -WorkingDirectory $InstallRoot -Credential $credential -Wait -PassThru
if ($registration.ExitCode -ne 0 -or -not (Test-Path -LiteralPath $runnerStatePath)) {
throw "Runner registration failed with exit code $($registration.ExitCode)."
}
$action = New-ScheduledTaskAction -Execute $runnerPath -Argument "--config `"$configPath`" daemon" -WorkingDirectory $InstallRoot
$trigger = New-ScheduledTaskTrigger -AtStartup
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -User $accountQualifiedName -Password $password -RunLevel Limited -Description "Runs the repository-scoped OCSentinel Gitea Actions runner." -Force | Out-Null
Start-ScheduledTask -TaskName $taskName
Start-Sleep -Seconds 3
$task = Get-ScheduledTask -TaskName $taskName
Write-Host "Gitea runner installed successfully."
Write-Host "Runner: $RunnerName"
Write-Host "Labels: windows:host"
Write-Host "Task: $taskName ($($task.State))"
Write-Host "Install path: $InstallRoot"
}
catch {
if (Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue) {
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false
}
throw
}

View File

@@ -66,6 +66,7 @@ internal sealed class AttackScanner
{
SchemaVersion = "2.0",
MachineName = Environment.MachineName,
NinjaOne = GetNinjaOneContext(),
GeneratedAtLocal = generatedAtLocal,
GeneratedAtUtc = generatedAtUtc,
ClientVersion = BuildMetadata.Version,
@@ -89,6 +90,24 @@ internal sealed class AttackScanner
};
}
private static NinjaOneContext GetNinjaOneContext()
{
return new NinjaOneContext
{
OrganizationId = ReadEnvironmentVariable("NINJA_ORGANIZATION_ID"),
OrganizationName = ReadEnvironmentVariable("NINJA_ORGANIZATION_NAME"),
MachineId = ReadEnvironmentVariable("NINJA_AGENT_MACHINE_ID"),
NodeId = ReadEnvironmentVariable("NINJA_AGENT_NODE_ID"),
LocationId = ReadEnvironmentVariable("NINJA_LOCATION_ID"),
LocationName = ReadEnvironmentVariable("NINJA_LOCATION_NAME")
};
}
private static string ReadEnvironmentVariable(string name)
{
return Environment.GetEnvironmentVariable(name)?.Trim() ?? string.Empty;
}
private static ScannerConfiguration LoadConfiguration(ScanOptions options)
{
if (string.IsNullOrWhiteSpace(options.ConfigPath))

View File

@@ -5,7 +5,10 @@ internal static class ScanAndUploadCommand
public static int Execute(string[] args)
{
string outputPath = @"C:\ProgramData\OCSentinel\reports\latest.json";
string? clientConfigPath = null;
string? secretPath = null;
bool hasOutput = false;
var scanArgs = new List<string>();
for (int i = 0; i < args.Length; i++)
{
@@ -13,11 +16,26 @@ internal static class ScanAndUploadCommand
{
outputPath = args[i + 1];
hasOutput = true;
break;
scanArgs.Add(args[i]);
scanArgs.Add(args[++i]);
continue;
}
if (string.Equals(args[i], "--client-config", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
{
clientConfigPath = args[++i];
continue;
}
if (string.Equals(args[i], "--secret-path", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
{
secretPath = args[++i];
continue;
}
scanArgs.Add(args[i]);
}
List<string> scanArgs = [.. args];
if (!hasOutput)
{
scanArgs.Add("--output");
@@ -36,6 +54,16 @@ internal static class ScanAndUploadCommand
outputPath
};
if (!string.IsNullOrWhiteSpace(clientConfigPath))
{
uploadArgs.AddRange(["--client-config", clientConfigPath]);
}
if (!string.IsNullOrWhiteSpace(secretPath))
{
uploadArgs.AddRange(["--secret-path", secretPath]);
}
return UploadCommand.Execute([.. uploadArgs]);
}
}

View File

@@ -80,6 +80,9 @@ internal sealed record ScanResult
public string MachineName { get; init; } = string.Empty;
// Populated only for runs launched by NinjaOne automation.
public NinjaOneContext NinjaOne { get; init; } = new();
public DateTimeOffset GeneratedAtLocal { get; init; }
public DateTimeOffset GeneratedAtUtc { get; init; }
@@ -111,6 +114,21 @@ internal sealed record ScanResult
public List<string> Errors { get; init; } = [];
}
internal sealed record NinjaOneContext
{
public string OrganizationId { get; init; } = string.Empty;
public string OrganizationName { get; init; } = string.Empty;
public string MachineId { get; init; } = string.Empty;
public string NodeId { get; init; } = string.Empty;
public string LocationId { get; init; } = string.Empty;
public string LocationName { get; init; } = string.Empty;
}
internal sealed record ScanRuntimeMetadata
{
public DateTimeOffset StartedAtUtc { get; init; }

View File

@@ -9,7 +9,7 @@
<RootNamespace>OCSentinelCli</RootNamespace>
<Product>OfficeCom Sentinel</Product>
<Company>OfficeCom</Company>
<Version>1.2.3</Version>
<Version>1.2.9</Version>
<AssemblyVersion>1.2.3.0</AssemblyVersion>
<FileVersion>1.2.3.0</FileVersion>
<InformationalVersion>1.2.3</InformationalVersion>