Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40c6daada8 | ||
|
|
5be4fb6c33 | ||
|
|
0fe8a96057 | ||
|
|
eb621ace5a | ||
|
|
e997e6b58c | ||
|
|
3f09805999 | ||
|
|
85f0682769 | ||
|
|
6722b00ee7 | ||
|
|
d3897c8e69 | ||
|
|
f9941b0807 | ||
|
|
c768e1be6b |
@@ -45,6 +45,39 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
dotnet-version: "10.0.x"
|
dotnet-version: "10.0.x"
|
||||||
|
|
||||||
|
- name: Install PowerShell
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if command -v pwsh >/dev/null 2>&1; then
|
||||||
|
pwsh --version
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$(id -u)" -eq 0 ]; then
|
||||||
|
SUDO=""
|
||||||
|
elif command -v sudo >/dev/null 2>&1; then
|
||||||
|
SUDO="sudo"
|
||||||
|
else
|
||||||
|
echo "PowerShell is missing and this runner cannot install packages."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
. /etc/os-release
|
||||||
|
case "$ID" in
|
||||||
|
ubuntu) MICROSOFT_REPO="https://packages.microsoft.com/config/ubuntu/${VERSION_ID}/packages-microsoft-prod.deb" ;;
|
||||||
|
debian) MICROSOFT_REPO="https://packages.microsoft.com/config/debian/${VERSION_ID}/packages-microsoft-prod.deb" ;;
|
||||||
|
*) echo "Unsupported runner distribution: $ID"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
$SUDO apt-get update
|
||||||
|
$SUDO apt-get install -y ca-certificates curl
|
||||||
|
curl -fsSL "$MICROSOFT_REPO" -o /tmp/packages-microsoft-prod.deb
|
||||||
|
$SUDO dpkg -i /tmp/packages-microsoft-prod.deb
|
||||||
|
$SUDO apt-get update
|
||||||
|
$SUDO apt-get install -y powershell
|
||||||
|
pwsh --version
|
||||||
|
|
||||||
- name: Build client package
|
- name: Build client package
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
@@ -62,16 +95,6 @@ jobs:
|
|||||||
$artifactUrl = "https://gitea.officecom.cloud/officecom/oc-sentinel/releases/download/$tag/OCSentinelClient-win-x64.zip"
|
$artifactUrl = "https://gitea.officecom.cloud/officecom/oc-sentinel/releases/download/$tag/OCSentinelClient-win-x64.zip"
|
||||||
./build/build-release-manifest.ps1 -ArtifactUrl $artifactUrl
|
./build/build-release-manifest.ps1 -ArtifactUrl $artifactUrl
|
||||||
|
|
||||||
- name: Upload package artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: ocsentinel-client-${{ github.sha }}
|
|
||||||
path: |
|
|
||||||
artifacts/OCSentinelClient-win-x64.zip
|
|
||||||
artifacts/OCSentinelClient-win-x64.zip.sha256
|
|
||||||
artifacts/version.json
|
|
||||||
if-no-files-found: warn
|
|
||||||
|
|
||||||
- name: Publish Gitea release assets
|
- name: Publish Gitea release assets
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -4,6 +4,12 @@
|
|||||||
"lookbackDays": 7,
|
"lookbackDays": 7,
|
||||||
"topFindings": 10,
|
"topFindings": 10,
|
||||||
"n8nWebhookUrl": "http://172.16.41.197:5678/webhook/ocsentinel-ingest",
|
"n8nWebhookUrl": "http://172.16.41.197:5678/webhook/ocsentinel-ingest",
|
||||||
|
"ninjaOrganizationId": "",
|
||||||
|
"ninjaOrganizationName": "",
|
||||||
|
"ninjaMachineId": "",
|
||||||
|
"ninjaNodeId": "",
|
||||||
|
"ninjaLocationId": "",
|
||||||
|
"ninjaLocationName": "",
|
||||||
"deviceIdentifierMode": "machineName",
|
"deviceIdentifierMode": "machineName",
|
||||||
"uploadTimeoutSeconds": 30,
|
"uploadTimeoutSeconds": 30,
|
||||||
"uploadQueueMaxReports": 100,
|
"uploadQueueMaxReports": 100,
|
||||||
|
|||||||
@@ -4,6 +4,12 @@
|
|||||||
"lookbackDays": 7,
|
"lookbackDays": 7,
|
||||||
"topFindings": 10,
|
"topFindings": 10,
|
||||||
"n8nWebhookUrl": "https://n8n.example.com/webhook/ocsentinel-ingest",
|
"n8nWebhookUrl": "https://n8n.example.com/webhook/ocsentinel-ingest",
|
||||||
|
"ninjaOrganizationId": "",
|
||||||
|
"ninjaOrganizationName": "",
|
||||||
|
"ninjaMachineId": "",
|
||||||
|
"ninjaNodeId": "",
|
||||||
|
"ninjaLocationId": "",
|
||||||
|
"ninjaLocationName": "",
|
||||||
"deviceIdentifierMode": "machineName",
|
"deviceIdentifierMode": "machineName",
|
||||||
"uploadTimeoutSeconds": 30,
|
"uploadTimeoutSeconds": 30,
|
||||||
"uploadQueueMaxReports": 100,
|
"uploadQueueMaxReports": 100,
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ powershell -ExecutionPolicy Bypass -File .\build\build-release-manifest.ps1 `
|
|||||||
|
|
||||||
## Local Schedule And Burst Mode
|
## Local Schedule And Burst Mode
|
||||||
|
|
||||||
|
During a NinjaOne installation or update, OCSentinel stores the device's
|
||||||
|
NinjaOne organization, location, and device identifiers in its local client
|
||||||
|
configuration. Scheduled `SYSTEM` scans restore that context before creating a
|
||||||
|
report, so their uploads remain assigned to the correct organization.
|
||||||
|
|
||||||
The installer creates two Windows Scheduled Tasks running as `SYSTEM`:
|
The installer creates two Windows Scheduled Tasks running as `SYSTEM`:
|
||||||
|
|
||||||
- `OCSentinel Daily Scan`: runs once per day and uploads one signed report.
|
- `OCSentinel Daily Scan`: runs once per day and uploads one signed report.
|
||||||
|
|||||||
@@ -3,6 +3,4 @@ DB_PORT=5432
|
|||||||
DB_NAME=ocsentinel
|
DB_NAME=ocsentinel
|
||||||
DB_USER=ocsentinel_debug
|
DB_USER=ocsentinel_debug
|
||||||
DB_PASSWORD=replace-with-server-generated-password
|
DB_PASSWORD=replace-with-server-generated-password
|
||||||
DASHBOARD_USER=ocsentinel-debug
|
|
||||||
DASHBOARD_PASSWORD=replace-with-server-generated-password
|
|
||||||
DASHBOARD_CSRF_SECRET=replace-with-server-generated-secret
|
DASHBOARD_CSRF_SECRET=replace-with-server-generated-secret
|
||||||
|
|||||||
@@ -2,14 +2,17 @@ import hashlib
|
|||||||
import hmac
|
import hmac
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from functools import wraps
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
import psycopg
|
import psycopg
|
||||||
from flask import Flask, Response, abort, redirect, render_template, request, url_for
|
from flask import Flask, abort, redirect, render_template, request, url_for
|
||||||
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
CURRENT_EVENT_HOURS = 24
|
||||||
|
STALE_REPORT_HOURS = 36
|
||||||
|
|
||||||
|
|
||||||
def db_connection():
|
def db_connection():
|
||||||
return psycopg.connect(
|
return psycopg.connect(
|
||||||
@@ -22,20 +25,6 @@ def db_connection():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def requires_auth(view):
|
|
||||||
@wraps(view)
|
|
||||||
def wrapped(*args, **kwargs):
|
|
||||||
auth = __import__("flask").request.authorization
|
|
||||||
expected_user = os.environ["DASHBOARD_USER"]
|
|
||||||
expected_password = os.environ["DASHBOARD_PASSWORD"]
|
|
||||||
valid = auth and hmac.compare_digest(auth.username or "", expected_user) and hmac.compare_digest(auth.password or "", expected_password)
|
|
||||||
if not valid:
|
|
||||||
return Response("Authentication required", 401, {"WWW-Authenticate": 'Basic realm="OCSentinel Debug"'})
|
|
||||||
return view(*args, **kwargs)
|
|
||||||
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
|
|
||||||
def csrf_token():
|
def csrf_token():
|
||||||
secret = os.environ["DASHBOARD_CSRF_SECRET"].encode("utf-8")
|
secret = os.environ["DASHBOARD_CSRF_SECRET"].encode("utf-8")
|
||||||
return hmac.new(secret, b"recipient-rules", hashlib.sha256).hexdigest()
|
return hmac.new(secret, b"recipient-rules", hashlib.sha256).hexdigest()
|
||||||
@@ -47,8 +36,40 @@ def require_csrf():
|
|||||||
abort(400)
|
abort(400)
|
||||||
|
|
||||||
|
|
||||||
|
def event_metadata(payload):
|
||||||
|
latest_event = None
|
||||||
|
for event in (payload or {}).get("Events", []):
|
||||||
|
value = event.get("Timestamp")
|
||||||
|
if not value:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
timestamp = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
if timestamp.tzinfo is None:
|
||||||
|
timestamp = timestamp.replace(tzinfo=timezone.utc)
|
||||||
|
if latest_event is None or timestamp > latest_event:
|
||||||
|
latest_event = timestamp
|
||||||
|
|
||||||
|
if latest_event is None:
|
||||||
|
return {"is_current": False, "label": "keine Ereignisse", "timestamp": None}
|
||||||
|
|
||||||
|
age_seconds = max(0, int((datetime.now(timezone.utc) - latest_event.astimezone(timezone.utc)).total_seconds()))
|
||||||
|
if age_seconds < 3600:
|
||||||
|
age_label = f"vor {max(1, age_seconds // 60)} Min."
|
||||||
|
elif age_seconds < 86400:
|
||||||
|
age_label = f"vor {age_seconds // 3600} Std."
|
||||||
|
else:
|
||||||
|
age_label = f"vor {age_seconds // 86400} Tg."
|
||||||
|
|
||||||
|
return {
|
||||||
|
"is_current": age_seconds <= CURRENT_EVENT_HOURS * 3600,
|
||||||
|
"label": age_label,
|
||||||
|
"timestamp": latest_event,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
@requires_auth
|
|
||||||
def overview():
|
def overview():
|
||||||
with db_connection() as connection, connection.cursor() as cursor:
|
with db_connection() as connection, connection.cursor() as cursor:
|
||||||
cursor.execute("SELECT * FROM ocsentinel.organization_summary")
|
cursor.execute("SELECT * FROM ocsentinel.organization_summary")
|
||||||
@@ -57,11 +78,12 @@ def overview():
|
|||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
SELECT machine_name, organization_name, received_at, alert_state,
|
SELECT machine_name, organization_name, received_at, alert_state,
|
||||||
total_events, unique_ip_count, cve_total, cve_critical
|
total_events, unique_ip_count, cve_total, cve_critical, payload
|
||||||
FROM (
|
FROM (
|
||||||
SELECT machine_name, received_at, alert_state, total_events,
|
SELECT machine_name, received_at, alert_state, total_events,
|
||||||
unique_ip_count, cve_total, cve_critical,
|
unique_ip_count, cve_total, cve_critical,
|
||||||
payload #>> '{NinjaOne,OrganizationName}' AS organization_name
|
payload #>> '{NinjaOne,OrganizationName}' AS organization_name,
|
||||||
|
payload
|
||||||
FROM ocsentinel.current_device_status
|
FROM ocsentinel.current_device_status
|
||||||
) AS status
|
) AS status
|
||||||
ORDER BY received_at DESC NULLS LAST
|
ORDER BY received_at DESC NULLS LAST
|
||||||
@@ -70,6 +92,16 @@ def overview():
|
|||||||
)
|
)
|
||||||
reports = cursor.fetchall()
|
reports = cursor.fetchall()
|
||||||
|
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT count(*) AS known_devices,
|
||||||
|
count(*) FILTER (WHERE received_at >= now() - interval '36 hours') AS reporting_devices,
|
||||||
|
count(*) FILTER (WHERE received_at IS NULL OR received_at < now() - interval '36 hours') AS stale_devices
|
||||||
|
FROM ocsentinel.current_device_status
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
coverage = cursor.fetchone()
|
||||||
|
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
SELECT machine_name, alert_state, total_events, unique_ip_count,
|
SELECT machine_name, alert_state, total_events, unique_ip_count,
|
||||||
@@ -81,11 +113,46 @@ def overview():
|
|||||||
)
|
)
|
||||||
alerts = cursor.fetchall()
|
alerts = cursor.fetchall()
|
||||||
|
|
||||||
return render_template("overview.html", summary=summary, reports=reports, alerts=alerts)
|
report_rows = []
|
||||||
|
for row in reports:
|
||||||
|
event = event_metadata(row[8])
|
||||||
|
report_rows.append(
|
||||||
|
{
|
||||||
|
"machine_name": row[0],
|
||||||
|
"organization_name": row[1],
|
||||||
|
"received_at": row[2],
|
||||||
|
"alert_state": row[3],
|
||||||
|
"total_events": row[4],
|
||||||
|
"unique_ip_count": row[5],
|
||||||
|
"event": event,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
alert_rows = []
|
||||||
|
for row in alerts:
|
||||||
|
event = event_metadata(row[5])
|
||||||
|
alert_rows.append(
|
||||||
|
{
|
||||||
|
"machine_name": row[0],
|
||||||
|
"alert_state": row[1],
|
||||||
|
"total_events": row[2],
|
||||||
|
"unique_ip_count": row[3],
|
||||||
|
"received_at": row[4],
|
||||||
|
"event": event,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"overview.html",
|
||||||
|
summary=summary,
|
||||||
|
coverage=coverage,
|
||||||
|
reports=report_rows,
|
||||||
|
alerts=alert_rows,
|
||||||
|
current_alert_count=sum(alert["event"]["is_current"] for alert in alert_rows),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/device/<machine_name>")
|
@app.get("/device/<machine_name>")
|
||||||
@requires_auth
|
|
||||||
def device(machine_name):
|
def device(machine_name):
|
||||||
with db_connection() as connection, connection.cursor() as cursor:
|
with db_connection() as connection, connection.cursor() as cursor:
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
@@ -104,11 +171,16 @@ def device(machine_name):
|
|||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
payload = report[12]
|
payload = report[12]
|
||||||
return render_template("device.html", report=report, payload=payload, payload_pretty=json.dumps(payload, indent=2, ensure_ascii=False))
|
return render_template(
|
||||||
|
"device.html",
|
||||||
|
report=report,
|
||||||
|
event=event_metadata(payload),
|
||||||
|
payload=payload,
|
||||||
|
payload_pretty=json.dumps(payload, indent=2, ensure_ascii=False),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/reports")
|
@app.get("/reports")
|
||||||
@requires_auth
|
|
||||||
def reports():
|
def reports():
|
||||||
with db_connection() as connection, connection.cursor() as cursor:
|
with db_connection() as connection, connection.cursor() as cursor:
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
@@ -126,7 +198,6 @@ def reports():
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/reports/<int:report_id>")
|
@app.get("/reports/<int:report_id>")
|
||||||
@requires_auth
|
|
||||||
def weekly_report(report_id):
|
def weekly_report(report_id):
|
||||||
with db_connection() as connection, connection.cursor() as cursor:
|
with db_connection() as connection, connection.cursor() as cursor:
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
@@ -147,7 +218,6 @@ def weekly_report(report_id):
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/recipients")
|
@app.get("/recipients")
|
||||||
@requires_auth
|
|
||||||
def recipients():
|
def recipients():
|
||||||
with db_connection() as connection, connection.cursor() as cursor:
|
with db_connection() as connection, connection.cursor() as cursor:
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
@@ -173,7 +243,6 @@ def recipients():
|
|||||||
|
|
||||||
|
|
||||||
@app.post("/recipients")
|
@app.post("/recipients")
|
||||||
@requires_auth
|
|
||||||
def add_recipient():
|
def add_recipient():
|
||||||
require_csrf()
|
require_csrf()
|
||||||
organization_id = request.form.get("organization_id", "").strip()
|
organization_id = request.form.get("organization_id", "").strip()
|
||||||
@@ -198,7 +267,6 @@ def add_recipient():
|
|||||||
|
|
||||||
|
|
||||||
@app.post("/recipients/<int:rule_id>/toggle")
|
@app.post("/recipients/<int:rule_id>/toggle")
|
||||||
@requires_auth
|
|
||||||
def toggle_recipient(rule_id):
|
def toggle_recipient(rule_id):
|
||||||
require_csrf()
|
require_csrf()
|
||||||
with db_connection() as connection, connection.cursor() as cursor:
|
with db_connection() as connection, connection.cursor() as cursor:
|
||||||
@@ -211,7 +279,6 @@ def toggle_recipient(rule_id):
|
|||||||
|
|
||||||
|
|
||||||
@app.post("/recipients/<int:rule_id>/delete")
|
@app.post("/recipients/<int:rule_id>/delete")
|
||||||
@requires_auth
|
|
||||||
def delete_recipient(rule_id):
|
def delete_recipient(rule_id):
|
||||||
require_csrf()
|
require_csrf()
|
||||||
with db_connection() as connection, connection.cursor() as cursor:
|
with db_connection() as connection, connection.cursor() as cursor:
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
:root { --ink:#17201d; --muted:#66736d; --paper:#f5f3eb; --panel:#fffdf7; --line:#d8d4c6; --green:#236342; --lime:#c7ee6b; --amber:#b86613; --red:#a8342b; }
|
:root { --ink:#17201d; --muted:#66736d; --paper:#f5f3eb; --panel:#fffdf7; --line:#d8d4c6; --green:#236342; --lime:#c7ee6b; --amber:#b86613; --red:#a8342b; }
|
||||||
* { box-sizing:border-box; }
|
* { box-sizing:border-box; }
|
||||||
body { margin:0; color:var(--ink); background:radial-gradient(circle at 86% -10%, #d6efad 0, transparent 28rem), var(--paper); font-family:Georgia, 'Times New Roman', serif; }.app-shell:before { content:''; position:fixed; z-index:-1; inset:0; opacity:.28; background-image:linear-gradient(rgba(35,99,66,.06) 1px,transparent 1px),linear-gradient(90deg,rgba(35,99,66,.06) 1px,transparent 1px); background-size:34px 34px; mask-image:linear-gradient(to bottom,black,transparent 62%); }
|
body { margin:0; color:var(--ink); background:radial-gradient(circle at 86% -10%, #d6efad 0, transparent 28rem), var(--paper); font-family:'Roboto',sans-serif; }.app-shell:before { content:''; position:fixed; z-index:-1; inset:0; opacity:.28; background-image:linear-gradient(rgba(35,99,66,.06) 1px,transparent 1px),linear-gradient(90deg,rgba(35,99,66,.06) 1px,transparent 1px); background-size:34px 34px; mask-image:linear-gradient(to bottom,black,transparent 62%); }
|
||||||
.masthead { height:70px; padding:0 6vw; display:flex; align-items:center; justify-content:space-between; border-bottom:1px solid var(--line); background:rgba(255,253,247,.88); box-shadow:0 4px 22px rgba(35,56,42,.06); backdrop-filter:blur(14px); }.header-links { display:flex; gap:8px; align-items:center; }.header-links a { padding:7px 9px; border-radius:7px; color:var(--muted); font:700 12px Arial,sans-serif; text-decoration:none; transition:background .18s ease,color .18s ease; }.header-links a:hover,.header-links a.active { color:var(--green); background:#e6f1e9; }
|
.masthead { height:70px; padding:0 6vw; display:flex; align-items:center; justify-content:flex-end; border-bottom:1px solid var(--line); background:rgba(255,253,247,.88); box-shadow:0 4px 22px rgba(35,56,42,.06); backdrop-filter:blur(14px); }.header-links { display:flex; gap:8px; align-items:center; }.header-links a { padding:7px 9px; border-radius:7px; color:var(--muted); font:700 12px 'Roboto',sans-serif; text-decoration:none; transition:background .18s ease,color .18s ease; }.header-links a:hover,.header-links a.active { color:var(--green); background:#e6f1e9; }
|
||||||
.brand { color:var(--ink); font:700 20px/1 Arial,sans-serif; text-decoration:none; letter-spacing:-.04em; }.brand span { display:inline-grid; place-items:center; margin-right:7px; width:28px; height:28px; background:var(--green); color:#fff; border-radius:50%; font-size:11px; letter-spacing:0; }.badge,.eyebrow { color:var(--muted); font:700 10px/1 Arial,sans-serif; text-transform:uppercase; letter-spacing:.12em; }.badge { border:1px solid var(--line); padding:6px 8px; border-radius:20px; }
|
.brand { color:var(--ink); font:700 20px/1 'Roboto',sans-serif; text-decoration:none; letter-spacing:-.04em; }.brand span { display:inline-grid; place-items:center; margin-right:7px; width:28px; height:28px; background:var(--green); color:#fff; border-radius:50%; font-size:11px; letter-spacing:0; }.badge,.eyebrow { color:var(--muted); font:700 10px/1 'Roboto',sans-serif; text-transform:uppercase; letter-spacing:.12em; }.badge { border:1px solid var(--line); padding:6px 8px; border-radius:20px; }
|
||||||
main { max-width:1280px; margin:auto; padding:58px 6vw 80px; }.hero { max-width:760px; margin-bottom:32px; }.hero h1 { font-size:clamp(34px,5vw,64px); line-height:.98; letter-spacing:-.06em; margin:10px 0; }.hero p { color:var(--muted); font-size:18px; }.hero.compact h1 { font-size:48px; }.hero-note { display:flex; align-items:center; gap:8px; margin-top:20px; color:var(--green); font:700 11px Arial,sans-serif; letter-spacing:.03em; }.hero-note span { width:8px; height:8px; border-radius:50%; background:var(--lime); box-shadow:0 0 0 4px rgba(199,238,107,.25); }
|
main { max-width:1280px; margin:auto; padding:32px 6vw 80px; }.hero { max-width:760px; margin-bottom:32px; }.hero h1 { font-size:clamp(34px,5vw,64px); line-height:.98; letter-spacing:-.06em; margin:10px 0; }.hero p { color:var(--muted); font-size:18px; }.hero.compact h1 { font-size:48px; }.hero-note { display:flex; align-items:center; gap:8px; margin-top:20px; color:var(--green); font:700 11px 'Roboto',sans-serif; letter-spacing:.03em; }.hero-note span { width:8px; height:8px; border-radius:50%; background:var(--lime); box-shadow:0 0 0 4px rgba(199,238,107,.25); }
|
||||||
.metrics { display:grid; grid-template-columns:repeat(5,1fr); gap:10px; margin:25px 0 46px; background:transparent; }.metrics article { min-height:130px; padding:20px; border:1px solid var(--line); border-radius:5px; background:var(--panel); box-shadow:0 5px 16px rgba(35,56,42,.035); transition:transform .18s ease,box-shadow .18s ease; }.metrics article:hover { transform:translateY(-3px); box-shadow:0 12px 24px rgba(35,56,42,.09); }.metrics span { display:block; color:var(--muted); font:700 10px Arial,sans-serif; letter-spacing:.09em; text-transform:uppercase; }.metrics strong { display:block; margin-top:16px; font:700 31px Arial,sans-serif; letter-spacing:-.05em; }.metrics .timestamp { font-size:14px; line-height:1.25; letter-spacing:-.02em; }.warning { color:var(--amber); }.critical { color:var(--red); }
|
.metrics { display:grid; grid-template-columns:repeat(5,1fr); gap:10px; margin:25px 0 46px; background:transparent; }.metrics article { min-height:130px; padding:20px; border:1px solid var(--line); border-radius:5px; background:var(--panel); box-shadow:0 5px 16px rgba(35,56,42,.035); transition:transform .18s ease,box-shadow .18s ease; }.metrics article:hover { transform:translateY(-3px); box-shadow:0 12px 24px rgba(35,56,42,.09); }.metrics span { display:block; color:var(--muted); font:700 10px 'Roboto',sans-serif; letter-spacing:.09em; text-transform:uppercase; }.metrics strong { display:block; margin-top:16px; font:700 31px 'Roboto',sans-serif; letter-spacing:-.05em; }.metrics .timestamp { font-size:14px; line-height:1.25; letter-spacing:-.02em; }.warning { color:var(--amber); }.critical { color:var(--red); }
|
||||||
.situation { display:flex; align-items:center; justify-content:space-between; gap:22px; margin:0 0 24px; padding:20px 22px; border:1px solid #b9d8c2; background:#edf8f0; color:#195235; }.situation.warning { border-color:#f2cf99; background:#fff6e8; color:#80450d; }.situation.critical { border-color:#edb4aa; background:#fff0ed; color:#8a2a20; }.situation strong { display:block; margin-top:7px; font:700 19px/1.15 Arial,sans-serif; letter-spacing:-.025em; }.situation > span { padding:7px 9px; border:1px solid currentColor; border-radius:20px; font:700 10px Arial,sans-serif; letter-spacing:.1em; }
|
.situation { display:flex; align-items:center; justify-content:space-between; gap:22px; margin:0 0 24px; padding:20px 22px; border:1px solid #b9d8c2; background:#edf8f0; color:#195235; }.situation.warning { border-color:#f2cf99; background:#fff6e8; color:#80450d; }.situation.critical { border-color:#edb4aa; background:#fff0ed; color:#8a2a20; }.situation strong { display:block; margin-top:7px; font:700 19px/1.15 'Roboto',sans-serif; letter-spacing:-.025em; }.situation > span { padding:7px 9px; border:1px solid currentColor; border-radius:20px; font:700 10px 'Roboto',sans-serif; letter-spacing:.1em; }
|
||||||
.panel { margin-top:26px; padding:26px; background:var(--panel); border:1px solid var(--line); border-radius:5px; box-shadow:0 6px 18px rgba(35,56,42,.035); }.panel-heading h2 { margin:8px 0 22px; font-size:28px; letter-spacing:-.04em; }.alert-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(210px,1fr)); gap:12px; }.alert-card { padding:17px; border-left:5px solid var(--amber); border-radius:3px; background:#fff7e9; color:var(--ink); text-decoration:none; transition:transform .18s ease,box-shadow .18s ease; }.alert-card:hover { transform:translateY(-2px); box-shadow:0 9px 18px rgba(88,57,20,.12); }.alert-card.critical { border-color:var(--red); background:#fff0ed; }.alert-card span,.alert-card small { display:block; font:700 10px Arial,sans-serif; letter-spacing:.08em; text-transform:uppercase; }.alert-card strong { display:block; margin:10px 0; font:700 22px Arial,sans-serif; letter-spacing:-.04em; }
|
.panel { margin-top:26px; padding:26px; background:var(--panel); border:1px solid var(--line); border-radius:5px; box-shadow:0 6px 18px rgba(35,56,42,.035); }.panel-heading h2 { margin:8px 0 22px; font-size:28px; letter-spacing:-.04em; }.panel-heading h2 small { color:var(--muted); font-size:12px; font-weight:500; letter-spacing:0; }.alert-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(210px,1fr)); gap:12px; }.alert-card { padding:17px; border-left:5px solid var(--amber); border-radius:3px; background:#fff7e9; color:var(--ink); text-decoration:none; transition:transform .18s ease,box-shadow .18s ease; }.alert-card:hover { transform:translateY(-2px); box-shadow:0 9px 18px rgba(88,57,20,.12); }.alert-card.critical { border-color:var(--red); background:#fff0ed; }.alert-card span,.alert-card small { display:block; font:700 10px 'Roboto',sans-serif; letter-spacing:.08em; text-transform:uppercase; }.alert-card strong { display:block; margin:10px 0; font:700 22px 'Roboto',sans-serif; letter-spacing:-.04em; }
|
||||||
table { width:100%; border-collapse:collapse; font-family:Arial,sans-serif; font-size:13px; } th { text-align:left; color:var(--muted); font-size:10px; letter-spacing:.1em; text-transform:uppercase; } th,td { padding:13px 8px; border-bottom:1px solid var(--line); } td a { color:var(--green); font-weight:700; text-decoration:none; }.state { display:inline-block; padding:4px 7px; border-radius:12px; background:#e2efe6; color:var(--green); font:700 10px Arial,sans-serif; text-transform:uppercase; }.state.warning { background:#fff0d7; color:var(--amber); }.state.critical { background:#ffe0db; color:var(--red); } pre { margin:0; padding:18px; overflow:auto; color:#dce7da; background:#13221b; border-radius:4px; font:12px/1.5 'Cascadia Code',Consolas,monospace; }.table-wrap { overflow:auto; }
|
.coverage-panel { padding-bottom:22px; }.coverage-metrics { display:grid; grid-template-columns:repeat(3,1fr); gap:10px; }.coverage-metrics article { padding:15px; border:1px solid var(--line); border-radius:4px; background:#faf9f4; }.coverage-metrics span { display:block; color:var(--muted); font:700 10px 'Roboto',sans-serif; letter-spacing:.08em; text-transform:uppercase; }.coverage-metrics strong { display:block; margin-top:8px; font-size:26px; }.ok { color:var(--green); }
|
||||||
|
table { width:100%; border-collapse:collapse; font-family:'Roboto',sans-serif; font-size:13px; } th { text-align:left; color:var(--muted); font-size:10px; letter-spacing:.1em; text-transform:uppercase; } th,td { padding:13px 8px; border-bottom:1px solid var(--line); } td a { color:var(--green); font-weight:700; text-decoration:none; }.state { display:inline-block; margin:1px 3px 1px 0; padding:4px 7px; border-radius:12px; background:#e2efe6; color:var(--green); font:700 10px 'Roboto',sans-serif; text-transform:uppercase; }.state.warning { background:#fff0d7; color:var(--amber); }.state.critical { background:#ffe0db; color:var(--red); }.state.current { background:#e2efe6; color:var(--green); }.state.historic { background:#ece9e1; color:#68736e; } pre { margin:0; padding:18px; overflow:auto; color:#dce7da; background:#13221b; border-radius:4px; font:12px/1.5 'Cascadia Code',Consolas,monospace; }.table-wrap { overflow:auto; }
|
||||||
.report-frame { background:#fff; border:1px solid var(--line); box-shadow:0 12px 40px rgba(20,35,27,.1); }
|
.report-frame { background:#fff; border:1px solid var(--line); box-shadow:0 12px 40px rgba(20,35,27,.1); }
|
||||||
.panel-heading p:last-child { max-width:720px; margin:-13px 0 20px; color:var(--muted); font-size:14px; }.calm-panel { border-color:#b9d8c2; background:#f4fbf5; }
|
.panel-heading p:last-child { max-width:720px; margin:-13px 0 20px; color:var(--muted); font-size:14px; }.calm-panel { border-color:#b9d8c2; background:#f4fbf5; }
|
||||||
.recipient-form { display:grid; grid-template-columns:minmax(220px,1fr) minmax(260px,1fr) auto; gap:14px; align-items:end; }.recipient-form label { display:grid; gap:6px; color:var(--muted); font:700 10px Arial,sans-serif; letter-spacing:.08em; text-transform:uppercase; }.recipient-form input,.recipient-form select { min-height:40px; padding:9px 10px; border:1px solid var(--line); border-radius:4px; background:#fff; color:var(--ink); font:14px Arial,sans-serif; }.recipient-form button,.rule-actions button { min-height:40px; padding:9px 13px; border:1px solid var(--green); border-radius:4px; background:var(--green); color:#fff; cursor:pointer; font:700 12px Arial,sans-serif; }.rule-actions { display:flex; gap:8px; }.rule-actions form { margin:0; }.rule-actions .button-secondary { border-color:#d8d4c6; background:#fffdf7; color:var(--ink); }.rule-actions .button-danger { border-color:#e3afa7; background:#fff0ed; color:#8a2a20; }
|
.recipient-form { display:grid; grid-template-columns:minmax(220px,1fr) minmax(260px,1fr) auto; gap:14px; align-items:end; }.recipient-form label { display:grid; gap:6px; color:var(--muted); font:700 10px 'Roboto',sans-serif; letter-spacing:.08em; text-transform:uppercase; }.recipient-form input,.recipient-form select { min-height:40px; padding:9px 10px; border:1px solid var(--line); border-radius:4px; background:#fff; color:var(--ink); font:14px 'Roboto',sans-serif; }.recipient-form button,.rule-actions button { min-height:40px; padding:9px 13px; border:1px solid var(--green); border-radius:4px; background:var(--green); color:#fff; cursor:pointer; font:700 12px 'Roboto',sans-serif; }.rule-actions { display:flex; gap:8px; }.rule-actions form { margin:0; }.rule-actions .button-secondary { border-color:#d8d4c6; background:#fffdf7; color:var(--ink); }.rule-actions .button-danger { border-color:#e3afa7; background:#fff0ed; color:#8a2a20; }
|
||||||
@media (max-width:850px) { .recipient-form { grid-template-columns:1fr; }.rule-actions { min-width:220px; } }
|
@media (max-width:850px) { .recipient-form { grid-template-columns:1fr; }.rule-actions { min-width:220px; } }
|
||||||
@media (max-width:850px) { .metrics { grid-template-columns:repeat(2,1fr); }.metrics article:last-child { grid-column:span 2; }.masthead { height:auto; min-height:70px; padding:14px 5vw; align-items:flex-start; }.header-links { justify-content:flex-end; flex-wrap:wrap; }.badge { display:none; } main { padding:38px 5vw; }.situation { align-items:flex-start; flex-direction:column; } }
|
@media (max-width:850px) { .metrics { grid-template-columns:repeat(2,1fr); }.metrics article:last-child { grid-column:span 2; }.masthead { height:auto; min-height:70px; padding:14px 5vw; align-items:flex-start; }.header-links { justify-content:flex-end; flex-wrap:wrap; }.badge { display:none; } main { padding:38px 5vw; }.situation { align-items:flex-start; flex-direction:column; } }
|
||||||
|
|||||||
@@ -3,13 +3,15 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>{% block title %}OCSentinel Debug{% endblock %}</title>
|
<title>{% block title %}OC Sentinel{% endblock %}</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='app.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='app.css') }}">
|
||||||
</head>
|
</head>
|
||||||
<body class="app-shell">
|
<body class="app-shell">
|
||||||
<header class="masthead">
|
<header class="masthead">
|
||||||
<a href="/" class="brand"><span>OC</span> Sentinel</a>
|
<nav class="header-links"><a class="{{ 'active' if request.endpoint == 'overview' else '' }}" href="/">Uebersicht</a><a class="{{ 'active' if request.endpoint in ('reports', 'weekly_report') else '' }}" href="{{ url_for('reports') }}">Berichte</a><a class="{{ 'active' if request.endpoint in ('recipients', 'add_recipient', 'toggle_recipient', 'delete_recipient') else '' }}" href="{{ url_for('recipients') }}">Empfaenger</a></nav>
|
||||||
<div class="header-links"><a class="{{ 'active' if request.endpoint == 'overview' else '' }}" href="/">Sicherheitslage</a><a class="{{ 'active' if request.endpoint in ('reports', 'weekly_report') else '' }}" href="{{ url_for('reports') }}">Wochenberichte</a><a class="{{ 'active' if request.endpoint in ('recipients', 'add_recipient', 'toggle_recipient', 'delete_recipient') else '' }}" href="{{ url_for('recipients') }}">Empfaenger</a><div class="badge">interner Sicherheitsbereich</div></div>
|
|
||||||
</header>
|
</header>
|
||||||
<main>{% block content %}{% endblock %}</main>
|
<main>{% block content %}{% endblock %}</main>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}{{ report[0] }} - OCSentinel{% endblock %}
|
{% block title %}{{ report[0] }} - OC Sentinel{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="hero compact"><p class="eyebrow">Geraetedetails</p><h1>{{ report[0] }}</h1><p><span class="state {{ report[6] }}">{{ report[6] }}</span> | Letzte Datenmeldung: {{ report[5] }}</p></section>
|
<section class="panel"><div class="panel-heading"><h2>{{ report[0] }}</h2><span class="state {{ report[6] }}">{{ report[6] }}</span>{% if report[8] %}<span class="state {{ 'current' if event.is_current else 'historic' }}">{{ 'aktuell' if event.is_current else 'historisch' }}: {{ event.label }}</span>{% endif %}</div></section>
|
||||||
<section class="metrics compact-metrics"><article><span>Ereignisse</span><strong>{{ report[8] }}</strong></article><article><span>Quell-IPs</span><strong>{{ report[9] }}</strong></article><article><span>CVEs</span><strong>{{ report[10] }}</strong></article><article><span>Kritische CVEs</span><strong class="critical">{{ report[11] }}</strong></article></section>
|
<section class="metrics compact-metrics"><article><span>Ereignisse</span><strong>{{ report[8] }}</strong></article><article><span>Quell-IPs</span><strong>{{ report[9] }}</strong></article><article><span>CVEs</span><strong>{{ report[10] }}</strong></article><article><span>Kritische CVEs</span><strong class="critical">{{ report[11] }}</strong></article></section>
|
||||||
<section class="panel"><div class="panel-heading"><p class="eyebrow">Technische Details</p><h2>Signierter Sicherheitsbericht</h2><p>Unveraenderte Rohdaten zur Nachvollziehbarkeit und Fehleranalyse.</p></div><pre>{{ payload_pretty }}</pre></section>
|
<section class="panel"><pre>{{ payload_pretty }}</pre></section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,43 +1,39 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="hero">
|
|
||||||
<p class="eyebrow">OCSentinel Sicherheitszentrale</p>
|
|
||||||
<h1>OfficeCom Sentinal Übersicht</h1>
|
|
||||||
<p>Aktuelle Auswertung aller signierten Sicherheitsberichte aus den betreuten Organisationen.</p>
|
|
||||||
<div class="hero-note"><span></span> Zentraler Ueberblick ueber Geraete, Ereignisse und Handlungsbedarf</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="situation {% if summary[2] %}critical{% elif summary[1] %}warning{% else %}ok{% endif %}">
|
<section class="situation {% if summary[2] %}critical{% elif summary[1] %}warning{% else %}ok{% endif %}">
|
||||||
<div><p class="eyebrow">Aktuelle Lage</p><strong>{% if summary[2] %}Kritische Ereignisse erfordern Aufmerksamkeit.{% elif summary[1] %}Hinweise vorhanden - bitte auffaellige Geraete pruefen.{% else %}Keine kritischen Auffaelligkeiten gemeldet.{% endif %}</strong></div>
|
<div><strong>{% if summary[2] %}Kritische Ereignisse{% elif summary[1] %}Hinweise vorhanden{% else %}Keine kritischen Auffaelligkeiten{% endif %}</strong></div>
|
||||||
<span>{% if summary[2] %}KRITISCH{% elif summary[1] %}PRUEFEN{% else %}STABIL{% endif %}</span>
|
<span>{% if summary[2] %}KRITISCH{% elif summary[1] %}PRUEFEN{% else %}STABIL{% endif %}</span>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="metrics">
|
<section class="metrics">
|
||||||
<article><span>Geraete mit Bericht</span><strong>{{ summary[0] }}</strong></article>
|
<article><span>Geraete</span><strong>{{ summary[0] }}</strong></article>
|
||||||
<article><span>Warnungen</span><strong class="warning">{{ summary[1] }}</strong></article>
|
<article><span>Warnungen</span><strong class="warning">{{ summary[1] }}</strong></article>
|
||||||
<article><span>Kritische Geraete</span><strong class="critical">{{ summary[2] }}</strong></article>
|
<article><span>Kritisch</span><strong class="critical">{{ summary[2] }}</strong></article>
|
||||||
<article><span>Erkannte Ereignisse</span><strong>{{ summary[3] }}</strong></article>
|
<article><span>Ereignisse</span><strong>{{ summary[3] }}</strong></article>
|
||||||
<article><span>Letzte Datenmeldung</span><strong class="timestamp">{{ summary[7] or 'noch keine Daten' }}</strong></article>
|
<article><span>Letzte Meldung</span><strong class="timestamp">{{ summary[7] or '-' }}</strong></article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel coverage-panel">
|
||||||
|
<div class="panel-heading"><h2>Geraeteabdeckung</h2></div>
|
||||||
|
<div class="coverage-metrics"><article><span>Bekannt</span><strong>{{ coverage[0] }}</strong></article><article><span>Meldend < 36 Std.</span><strong class="ok">{{ coverage[1] }}</strong></article><article><span>Stumm > 36 Std.</span><strong class="{% if coverage[2] %}warning{% endif %}">{{ coverage[2] }}</strong></article></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{% if alerts %}
|
{% if alerts %}
|
||||||
<section class="panel alert-panel">
|
<section class="panel alert-panel">
|
||||||
<div class="panel-heading"><p class="eyebrow">Handlungsbedarf</p><h2>Auffaellige Geraete</h2><p>Diese Geraete haben zuletzt Warnungen oder kritische Sicherheitsereignisse gemeldet.</p></div>
|
<div class="panel-heading"><h2>Auffaellige Geraete <small>{{ current_alert_count }} aktuell, {{ alerts|length - current_alert_count }} historisch</small></h2></div>
|
||||||
<div class="alert-grid">
|
<div class="alert-grid">
|
||||||
{% for alert in alerts %}
|
{% for alert in alerts %}
|
||||||
<a class="alert-card {{ alert[1] }}" href="{{ url_for('device', machine_name=alert[0]) }}">
|
<a class="alert-card {{ alert.alert_state }}" href="{{ url_for('device', machine_name=alert.machine_name) }}">
|
||||||
<span>{{ alert[1] }}</span><strong>{{ alert[0] }}</strong><small>{{ alert[2] }} Ereignisse | {{ alert[3] }} Quell-IPs | {{ alert[4] }}</small>
|
<span>{{ alert.alert_state }} | {{ 'aktuell' if alert.event.is_current else 'historisch' }}</span><strong>{{ alert.machine_name }}</strong><small>{{ alert.total_events }} Ereignisse | {{ alert.unique_ip_count }} Quell-IPs | letztes Ereignis {{ alert.event.label }}</small>
|
||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{% else %}
|
|
||||||
<section class="panel calm-panel"><div class="panel-heading"><p class="eyebrow">Handlungsbedarf</p><h2>Keine auffaelligen Geraete</h2><p>Die zuletzt eingegangenen Berichte enthalten keine Warnungen oder kritischen Ereignisse.</p></div></section>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<div class="panel-heading"><p class="eyebrow">Berichtsbestand</p><h2>Aktuelle Geraetestatus</h2><p>Jede Zeile zeigt den letzten erfolgreich uebermittelten OCSentinel-Bericht eines Geraets.</p></div>
|
<div class="panel-heading"><h2>Geraetestatus</h2></div>
|
||||||
<div class="table-wrap"><table><thead><tr><th>Geraet</th><th>Organisation</th><th>Status</th><th>Ereignisse</th><th>Quell-IPs</th><th>Empfangen</th></tr></thead>
|
<div class="table-wrap"><table><thead><tr><th>Geraet</th><th>Organisation</th><th>Status</th><th>Ereignisse</th><th>Quell-IPs</th><th>Empfangen</th></tr></thead>
|
||||||
<tbody>{% for row in reports %}<tr><td><a href="{{ url_for('device', machine_name=row[0]) }}">{{ row[0] }}</a></td><td>{{ row[1] or '-' }}</td><td><span class="state {{ row[3] }}">{{ row[3] }}</span></td><td>{{ row[4] }}</td><td>{{ row[5] }}</td><td>{{ row[2] or '-' }}</td></tr>{% else %}<tr><td colspan="6">Noch keine Geraeteberichte vorhanden.</td></tr>{% endfor %}</tbody></table></div>
|
<tbody>{% for row in reports %}<tr><td><a href="{{ url_for('device', machine_name=row.machine_name) }}">{{ row.machine_name }}</a></td><td>{{ row.organization_name or '-' }}</td><td><span class="state {{ row.alert_state }}">{{ row.alert_state }}</span>{% if row.total_events %}<span class="state {{ 'current' if row.event.is_current else 'historic' }}">{{ 'aktuell' if row.event.is_current else 'historisch' }}</span>{% endif %}</td><td>{{ row.total_events }}</td><td>{{ row.unique_ip_count }}</td><td>{{ row.received_at or '-' }}</td></tr>{% else %}<tr><td colspan="6">Keine Geraeteberichte.</td></tr>{% endfor %}</tbody></table></div>
|
||||||
</section>
|
</section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}Berichtsempfaenger - OCSentinel{% endblock %}
|
{% block title %}Empfaenger - OC Sentinel{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="hero compact"><p class="eyebrow">Wochenberichte</p><h1>Berichtsempfaenger</h1><p>Diese Regeln bestimmen, wer den Wochenbericht einer Organisation per E-Mail erhaelt.</p></section>
|
<section class="panel"><div class="panel-heading"><h2>Empfaenger hinzufuegen</h2></div>
|
||||||
|
|
||||||
<section class="panel"><div class="panel-heading"><p class="eyebrow">Neue Regel</p><h2>Empfaenger hinzufuegen</h2></div>
|
|
||||||
<form class="recipient-form" method="post" action="{{ url_for('add_recipient') }}">
|
<form class="recipient-form" method="post" action="{{ url_for('add_recipient') }}">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
<label>Organisation<select name="organization_id" id="organization_id" required onchange="document.getElementById('organization_name').value=this.options[this.selectedIndex].dataset.name"><option value="*" data-name="Alle Organisationen">Alle Organisationen</option>{% for organization in organizations %}<option value="{{ organization[0] }}" data-name="{{ organization[1] }}">{{ organization[1] }}</option>{% endfor %}</select></label>
|
<label>Organisation<select name="organization_id" id="organization_id" required onchange="document.getElementById('organization_name').value=this.options[this.selectedIndex].dataset.name"><option value="*" data-name="Alle Organisationen">Alle Organisationen</option>{% for organization in organizations %}<option value="{{ organization[0] }}" data-name="{{ organization[1] }}">{{ organization[1] }}</option>{% endfor %}</select></label>
|
||||||
<input type="hidden" name="organization_name" id="organization_name" value="Alle Organisationen">
|
<input type="hidden" name="organization_name" id="organization_name" value="Alle Organisationen">
|
||||||
<label>E-Mail-Adresse<input type="email" name="recipient_email" placeholder="name@officecom.it" required></label>
|
<label>E-Mail-Adresse<input type="email" name="recipient_email" placeholder="name@officecom.it" required></label>
|
||||||
<button type="submit">Empfaenger speichern</button>
|
<button type="submit">Speichern</button>
|
||||||
</form></section>
|
</form></section>
|
||||||
|
|
||||||
<section class="panel"><div class="panel-heading"><p class="eyebrow">Aktive Regeln</p><h2>E-Mail-Verteiler</h2><p>"Alle Organisationen" wird zu jedem organisationsspezifischen Verteiler hinzugefuegt.</p></div>
|
<section class="panel"><div class="panel-heading"><h2>E-Mail-Verteiler</h2></div>
|
||||||
<div class="table-wrap"><table><thead><tr><th>Organisation</th><th>E-Mail-Adresse</th><th>Status</th><th>Aktion</th></tr></thead><tbody>
|
<div class="table-wrap"><table><thead><tr><th>Organisation</th><th>E-Mail-Adresse</th><th>Status</th><th>Aktion</th></tr></thead><tbody>
|
||||||
{% for rule in rules %}<tr><td>{{ rule[2] }}</td><td>{{ rule[3] }}</td><td><span class="state {{ 'ok' if rule[4] else 'warning' }}">{{ 'aktiv' if rule[4] else 'pausiert' }}</span></td><td class="rule-actions"><form method="post" action="{{ url_for('toggle_recipient', rule_id=rule[0]) }}"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="button-secondary" type="submit">{{ 'Pausieren' if rule[4] else 'Aktivieren' }}</button></form><form method="post" action="{{ url_for('delete_recipient', rule_id=rule[0]) }}"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="button-danger" type="submit">Loeschen</button></form></td></tr>{% else %}<tr><td colspan="4">Noch keine Empfaengerregeln angelegt.</td></tr>{% endfor %}
|
{% for rule in rules %}<tr><td>{{ rule[2] }}</td><td>{{ rule[3] }}</td><td><span class="state {{ 'ok' if rule[4] else 'warning' }}">{{ 'aktiv' if rule[4] else 'pausiert' }}</span></td><td class="rule-actions"><form method="post" action="{{ url_for('toggle_recipient', rule_id=rule[0]) }}"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="button-secondary" type="submit">{{ 'Pausieren' if rule[4] else 'Aktivieren' }}</button></form><form method="post" action="{{ url_for('delete_recipient', rule_id=rule[0]) }}"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="button-danger" type="submit">Loeschen</button></form></td></tr>{% else %}<tr><td colspan="4">Keine Empfaengerregeln.</td></tr>{% endfor %}
|
||||||
</tbody></table></div></section>
|
</tbody></table></div></section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}Wochenberichte - OCSentinel Debug{% endblock %}
|
{% block title %}Berichte - OC Sentinel{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="hero compact"><p class="eyebrow">Archiv</p><h1>Wochenberichte</h1><p>Je Organisation automatisch durch n8n erzeugt.</p></section>
|
|
||||||
<section class="panel"><div class="table-wrap"><table><thead><tr><th>Organisation</th><th>Zeitraum</th><th>Geraete</th><th>Warnung</th><th>Kritisch</th><th>Events</th><th>Erstellt</th></tr></thead><tbody>
|
<section class="panel"><div class="table-wrap"><table><thead><tr><th>Organisation</th><th>Zeitraum</th><th>Geraete</th><th>Warnung</th><th>Kritisch</th><th>Events</th><th>Erstellt</th></tr></thead><tbody>
|
||||||
{% for row in reports %}<tr><td><a href="{{ url_for('weekly_report', report_id=row[0]) }}">{{ row[1] }}</a></td><td>{{ row[2] }} bis {{ row[3] }}</td><td>{{ row[5] }}</td><td>{{ row[6] }}</td><td>{{ row[7] }}</td><td>{{ row[8] }}</td><td>{{ row[4] }}</td></tr>{% else %}<tr><td colspan="7">Noch keine Wochenberichte erzeugt.</td></tr>{% endfor %}
|
{% for row in reports %}<tr><td><a href="{{ url_for('weekly_report', report_id=row[0]) }}">{{ row[1] }}</a></td><td>{{ row[2] }} bis {{ row[3] }}</td><td>{{ row[5] }}</td><td>{{ row[6] }}</td><td>{{ row[7] }}</td><td>{{ row[8] }}</td><td>{{ row[4] }}</td></tr>{% else %}<tr><td colspan="7">Keine Wochenberichte.</td></tr>{% endfor %}
|
||||||
</tbody></table></div></section>
|
</tbody></table></div></section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -39,6 +39,40 @@ function Resolve-PathLike {
|
|||||||
return [System.IO.Path]::GetFullPath((Join-Path $BasePath $PathValue))
|
return [System.IO.Path]::GetFullPath((Join-Path $BasePath $PathValue))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Restore-NinjaContextFromClientConfiguration {
|
||||||
|
param([Parameter(Mandatory)][string]$Path)
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $Path)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$clientConfiguration = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
|
||||||
|
$mappings = @(
|
||||||
|
@{ EnvironmentName = "NINJA_ORGANIZATION_ID"; PropertyName = "ninjaOrganizationId" },
|
||||||
|
@{ EnvironmentName = "NINJA_ORGANIZATION_NAME"; PropertyName = "ninjaOrganizationName" },
|
||||||
|
@{ EnvironmentName = "NINJA_AGENT_MACHINE_ID"; PropertyName = "ninjaMachineId" },
|
||||||
|
@{ EnvironmentName = "NINJA_AGENT_NODE_ID"; PropertyName = "ninjaNodeId" },
|
||||||
|
@{ EnvironmentName = "NINJA_LOCATION_ID"; PropertyName = "ninjaLocationId" },
|
||||||
|
@{ EnvironmentName = "NINJA_LOCATION_NAME"; PropertyName = "ninjaLocationName" }
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($mapping in $mappings) {
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($mapping.EnvironmentName, "Process"))) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = [string]$clientConfiguration.($mapping.PropertyName)
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($value)) {
|
||||||
|
[Environment]::SetEnvironmentVariable($mapping.EnvironmentName, $value, "Process")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Warning "Could not restore stored NinjaOne context: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $appExe)) {
|
if (-not (Test-Path $appExe)) {
|
||||||
throw "Application executable not found: $appExe"
|
throw "Application executable not found: $appExe"
|
||||||
}
|
}
|
||||||
@@ -54,6 +88,8 @@ else {
|
|||||||
$secretFullPath = if ([string]::IsNullOrWhiteSpace($SecretPath)) { "" } else { Resolve-PathLike -PathValue $SecretPath -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)
|
$canUpload = (Test-Path $clientConfigFullPath) -and (-not [string]::IsNullOrWhiteSpace($secretFullPath)) -and (Test-Path $secretFullPath)
|
||||||
|
|
||||||
|
Restore-NinjaContextFromClientConfiguration -Path $clientConfigFullPath
|
||||||
|
|
||||||
if ($UploadMode -eq "required" -and -not $canUpload) {
|
if ($UploadMode -eq "required" -and -not $canUpload) {
|
||||||
throw "UploadMode 'required' was set, but client config or protected secret is missing."
|
throw "UploadMode 'required' was set, but client config or protected secret is missing."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,39 @@ param(
|
|||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
function Initialize-OCSentinelTls {
|
||||||
|
$protocols = [Net.SecurityProtocolType]::Tls12
|
||||||
|
if ([Enum]::GetNames([Net.SecurityProtocolType]) -contains "Tls13") {
|
||||||
|
$protocols = $protocols -bor [Net.SecurityProtocolType]::Tls13
|
||||||
|
}
|
||||||
|
|
||||||
|
[Net.ServicePointManager]::SecurityProtocol = $protocols
|
||||||
|
[Net.ServicePointManager]::Expect100Continue = $false
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-OCSentinelManifest {
|
||||||
|
param([Parameter(Mandatory)][string]$Uri)
|
||||||
|
|
||||||
|
$parameters = @{ Method = "Get"; Uri = $Uri; TimeoutSec = 60 }
|
||||||
|
if ((Get-Command Invoke-RestMethod).Parameters.ContainsKey("UseBasicParsing")) {
|
||||||
|
$parameters.UseBasicParsing = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
for ($attempt = 1; $attempt -le 3; $attempt++) {
|
||||||
|
try {
|
||||||
|
return Invoke-RestMethod @parameters
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
if ($attempt -eq 3) {
|
||||||
|
throw "Could not retrieve the OCSentinel release manifest after 3 attempts. Verify that the device can reach gitea.officecom.cloud with TLS 1.2 or newer. Last error: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
Start-Sleep -Seconds (3 * $attempt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Initialize-OCSentinelTls
|
||||||
|
|
||||||
$installRoot = Join-Path ${env:ProgramFiles} "OCSentinel"
|
$installRoot = Join-Path ${env:ProgramFiles} "OCSentinel"
|
||||||
$appExe = Join-Path $installRoot "app\OCSentinelCli.exe"
|
$appExe = Join-Path $installRoot "app\OCSentinelCli.exe"
|
||||||
$installScript = Join-Path $installRoot "scripts\install-ocsentinel.ps1"
|
$installScript = Join-Path $installRoot "scripts\install-ocsentinel.ps1"
|
||||||
@@ -66,7 +99,7 @@ if ([string]::IsNullOrWhiteSpace($ManifestUrl)) {
|
|||||||
$resolvedManifestUrl = Resolve-ManifestUrl -ManifestUrl $ManifestUrl -Channel $Channel
|
$resolvedManifestUrl = Resolve-ManifestUrl -ManifestUrl $ManifestUrl -Channel $Channel
|
||||||
Write-Host "Checking update manifest: $resolvedManifestUrl"
|
Write-Host "Checking update manifest: $resolvedManifestUrl"
|
||||||
|
|
||||||
$manifest = Invoke-RestMethod -Method Get -Uri $resolvedManifestUrl -TimeoutSec 60
|
$manifest = Get-OCSentinelManifest -Uri $resolvedManifestUrl
|
||||||
if (-not $manifest.version -or -not $manifest.artifactUrl -or -not $manifest.sha256) {
|
if (-not $manifest.version -or -not $manifest.artifactUrl -or -not $manifest.sha256) {
|
||||||
throw "Update manifest is missing required fields: version, artifactUrl, sha256."
|
throw "Update manifest is missing required fields: version, artifactUrl, sha256."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"channel": "stable",
|
"channel": "stable",
|
||||||
"version": "1.3.3",
|
"version": "1.3.5",
|
||||||
"publishedAtUtc": "2026-07-26T18:25:32.8762198Z",
|
"publishedAtUtc": "2026-07-26T22:34:59.0988140Z",
|
||||||
"artifactUrl": "https://gitea.officecom.cloud/officecom/oc-sentinel/releases/download/v1.3.3/OCSentinelClient-win-x64.zip",
|
"artifactUrl": "https://gitea.officecom.cloud/officecom/oc-sentinel/releases/download/v1.3.5/OCSentinelClient-win-x64.zip",
|
||||||
"sha256": "9bb2b2dc25c59bc6faf53ded6095667ac73640038cfd426a74f3bf6aca81bd86",
|
"sha256": "4d53be28835e7bbe3be8dab7e68a47d4db00ebd4cf09cd8ced158cecd09f9490",
|
||||||
"minUpdaterVersion": "1.0.0"
|
"minUpdaterVersion": "1.0.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,67 @@ param(
|
|||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
$ProgressPreference = "SilentlyContinue"
|
$ProgressPreference = "SilentlyContinue"
|
||||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
|
||||||
|
function Initialize-OCSentinelTls {
|
||||||
|
$protocols = [Net.SecurityProtocolType]::Tls12
|
||||||
|
if ([Enum]::GetNames([Net.SecurityProtocolType]) -contains "Tls13") {
|
||||||
|
$protocols = $protocols -bor [Net.SecurityProtocolType]::Tls13
|
||||||
|
}
|
||||||
|
|
||||||
|
[Net.ServicePointManager]::SecurityProtocol = $protocols
|
||||||
|
[Net.ServicePointManager]::Expect100Continue = $false
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-OCSentinelManifest {
|
||||||
|
param([Parameter(Mandatory)][string]$Uri)
|
||||||
|
|
||||||
|
$parameters = @{ Method = "Get"; Uri = $Uri; TimeoutSec = 60 }
|
||||||
|
if ((Get-Command Invoke-RestMethod).Parameters.ContainsKey("UseBasicParsing")) {
|
||||||
|
$parameters.UseBasicParsing = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
for ($attempt = 1; $attempt -le 3; $attempt++) {
|
||||||
|
try {
|
||||||
|
return Invoke-RestMethod @parameters
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
if ($attempt -eq 3) {
|
||||||
|
throw "Could not retrieve the OCSentinel release manifest after 3 attempts. Verify that the device can reach gitea.officecom.cloud with TLS 1.2 or newer. Last error: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
Start-Sleep -Seconds (3 * $attempt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-OCSentinelUpdater {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)][string]$UpdaterPath,
|
||||||
|
[Parameter(Mandatory)][string]$ManifestUri
|
||||||
|
)
|
||||||
|
|
||||||
|
# Existing clients can still contain an older updater without TLS setup.
|
||||||
|
# Start it in a prepared child process so it can download the current package.
|
||||||
|
$escapedUpdaterPath = $UpdaterPath.Replace("'", "''")
|
||||||
|
$escapedManifestUri = $ManifestUri.Replace("'", "''")
|
||||||
|
$command = @"
|
||||||
|
`$protocols = [Net.SecurityProtocolType]::Tls12
|
||||||
|
if ([Enum]::GetNames([Net.SecurityProtocolType]) -contains 'Tls13') {
|
||||||
|
`$protocols = `$protocols -bor [Net.SecurityProtocolType]::Tls13
|
||||||
|
}
|
||||||
|
[Net.ServicePointManager]::SecurityProtocol = `$protocols
|
||||||
|
[Net.ServicePointManager]::Expect100Continue = `$false
|
||||||
|
& '$escapedUpdaterPath' -ManifestUrl '$escapedManifestUri'
|
||||||
|
exit `$LASTEXITCODE
|
||||||
|
"@
|
||||||
|
|
||||||
|
& powershell.exe -NoProfile -ExecutionPolicy Bypass -Command $command
|
||||||
|
$exitCode = $LASTEXITCODE
|
||||||
|
if ($exitCode -ne 0) {
|
||||||
|
throw "OCSentinel updater exited with code $exitCode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Initialize-OCSentinelTls
|
||||||
|
|
||||||
$installRoot = Join-Path $env:ProgramFiles "OCSentinel"
|
$installRoot = Join-Path $env:ProgramFiles "OCSentinel"
|
||||||
$updaterPath = Join-Path $installRoot "scripts\update-ocsentinel.ps1"
|
$updaterPath = Join-Path $installRoot "scripts\update-ocsentinel.ps1"
|
||||||
@@ -47,14 +107,11 @@ function Assert-ArtifactSignature {
|
|||||||
|
|
||||||
if (Test-Path -LiteralPath $updaterPath) {
|
if (Test-Path -LiteralPath $updaterPath) {
|
||||||
Write-Host "Existing OCSentinel installation found. Checking for updates."
|
Write-Host "Existing OCSentinel installation found. Checking for updates."
|
||||||
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $updaterPath -ManifestUrl $ManifestUrl
|
Invoke-OCSentinelUpdater -UpdaterPath $updaterPath -ManifestUri $ManifestUrl
|
||||||
if ($LASTEXITCODE -ne 0) {
|
|
||||||
throw "OCSentinel updater exited with code $LASTEXITCODE"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Write-Host "Reading OCSentinel release manifest: $ManifestUrl"
|
Write-Host "Reading OCSentinel release manifest: $ManifestUrl"
|
||||||
$manifest = Invoke-RestMethod -Method Get -Uri $ManifestUrl -TimeoutSec 60
|
$manifest = Get-OCSentinelManifest -Uri $ManifestUrl
|
||||||
if ([string]::IsNullOrWhiteSpace($manifest.version) -or [string]::IsNullOrWhiteSpace($manifest.artifactUrl) -or [string]::IsNullOrWhiteSpace($manifest.sha256)) {
|
if ([string]::IsNullOrWhiteSpace($manifest.version) -or [string]::IsNullOrWhiteSpace($manifest.artifactUrl) -or [string]::IsNullOrWhiteSpace($manifest.sha256)) {
|
||||||
throw "Release manifest is missing version, artifactUrl, or sha256."
|
throw "Release manifest is missing version, artifactUrl, or sha256."
|
||||||
}
|
}
|
||||||
@@ -107,8 +164,22 @@ if (-not [string]::IsNullOrWhiteSpace($WebhookUrl)) {
|
|||||||
$clientConfig = Get-Content -LiteralPath $clientConfigPath -Raw | ConvertFrom-Json
|
$clientConfig = Get-Content -LiteralPath $clientConfigPath -Raw | ConvertFrom-Json
|
||||||
$clientConfig.n8nWebhookUrl = $WebhookUrl
|
$clientConfig.n8nWebhookUrl = $WebhookUrl
|
||||||
$clientConfig.environment = "production"
|
$clientConfig.environment = "production"
|
||||||
|
$ninjaContext = @(
|
||||||
|
@{ EnvironmentName = "NINJA_ORGANIZATION_ID"; PropertyName = "ninjaOrganizationId" },
|
||||||
|
@{ EnvironmentName = "NINJA_ORGANIZATION_NAME"; PropertyName = "ninjaOrganizationName" },
|
||||||
|
@{ EnvironmentName = "NINJA_AGENT_MACHINE_ID"; PropertyName = "ninjaMachineId" },
|
||||||
|
@{ EnvironmentName = "NINJA_AGENT_NODE_ID"; PropertyName = "ninjaNodeId" },
|
||||||
|
@{ EnvironmentName = "NINJA_LOCATION_ID"; PropertyName = "ninjaLocationId" },
|
||||||
|
@{ EnvironmentName = "NINJA_LOCATION_NAME"; PropertyName = "ninjaLocationName" }
|
||||||
|
)
|
||||||
|
foreach ($entry in $ninjaContext) {
|
||||||
|
$value = [Environment]::GetEnvironmentVariable($entry.EnvironmentName, "Process")
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($value)) {
|
||||||
|
$clientConfig | Add-Member -NotePropertyName $entry.PropertyName -NotePropertyValue $value.Trim() -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
$clientConfig | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $clientConfigPath -Encoding UTF8
|
$clientConfig | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $clientConfigPath -Encoding UTF8
|
||||||
Write-Host "Configured OCSentinel upload endpoint."
|
Write-Host "Configured OCSentinel upload endpoint and NinjaOne context."
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not [string]::IsNullOrWhiteSpace($SecretValue)) {
|
if (-not [string]::IsNullOrWhiteSpace($SecretValue)) {
|
||||||
|
|||||||
@@ -7,7 +7,39 @@ param(
|
|||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
$ProgressPreference = "SilentlyContinue"
|
$ProgressPreference = "SilentlyContinue"
|
||||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
|
||||||
|
function Initialize-OCSentinelTls {
|
||||||
|
$protocols = [Net.SecurityProtocolType]::Tls12
|
||||||
|
if ([Enum]::GetNames([Net.SecurityProtocolType]) -contains "Tls13") {
|
||||||
|
$protocols = $protocols -bor [Net.SecurityProtocolType]::Tls13
|
||||||
|
}
|
||||||
|
|
||||||
|
[Net.ServicePointManager]::SecurityProtocol = $protocols
|
||||||
|
[Net.ServicePointManager]::Expect100Continue = $false
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-OCSentinelManifest {
|
||||||
|
param([Parameter(Mandatory)][string]$Uri)
|
||||||
|
|
||||||
|
$parameters = @{ Method = "Get"; Uri = $Uri; TimeoutSec = 60 }
|
||||||
|
if ((Get-Command Invoke-RestMethod).Parameters.ContainsKey("UseBasicParsing")) {
|
||||||
|
$parameters.UseBasicParsing = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
for ($attempt = 1; $attempt -le 3; $attempt++) {
|
||||||
|
try {
|
||||||
|
return Invoke-RestMethod @parameters
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
if ($attempt -eq 3) {
|
||||||
|
throw "Could not retrieve the OCSentinel release manifest after 3 attempts. Verify that the device can reach gitea.officecom.cloud with TLS 1.2 or newer. Last error: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
Start-Sleep -Seconds (3 * $attempt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Initialize-OCSentinelTls
|
||||||
|
|
||||||
function Get-NinjaValue {
|
function Get-NinjaValue {
|
||||||
param([Parameter(Mandatory)][string]$Name)
|
param([Parameter(Mandatory)][string]$Name)
|
||||||
@@ -26,7 +58,7 @@ if ([string]::IsNullOrWhiteSpace($WebhookUrl) -or [string]::IsNullOrWhiteSpace($
|
|||||||
throw "WebhookUrl and SecretValue must be set as NinjaOne script variables."
|
throw "WebhookUrl and SecretValue must be set as NinjaOne script variables."
|
||||||
}
|
}
|
||||||
|
|
||||||
$manifest = Invoke-RestMethod -Method Get -Uri $ManifestUrl -TimeoutSec 60
|
$manifest = Get-OCSentinelManifest -Uri $ManifestUrl
|
||||||
if ([string]::IsNullOrWhiteSpace($manifest.artifactUrl) -or [string]::IsNullOrWhiteSpace($manifest.sha256)) {
|
if ([string]::IsNullOrWhiteSpace($manifest.artifactUrl) -or [string]::IsNullOrWhiteSpace($manifest.sha256)) {
|
||||||
throw "The release manifest is incomplete."
|
throw "The release manifest is incomplete."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,40 @@ function Resolve-PathLike {
|
|||||||
return [System.IO.Path]::GetFullPath((Join-Path $BasePath $PathValue))
|
return [System.IO.Path]::GetFullPath((Join-Path $BasePath $PathValue))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Restore-NinjaContextFromClientConfiguration {
|
||||||
|
param([Parameter(Mandatory)][string]$Path)
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $Path)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$clientConfiguration = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
|
||||||
|
$mappings = @(
|
||||||
|
@{ EnvironmentName = "NINJA_ORGANIZATION_ID"; PropertyName = "ninjaOrganizationId" },
|
||||||
|
@{ EnvironmentName = "NINJA_ORGANIZATION_NAME"; PropertyName = "ninjaOrganizationName" },
|
||||||
|
@{ EnvironmentName = "NINJA_AGENT_MACHINE_ID"; PropertyName = "ninjaMachineId" },
|
||||||
|
@{ EnvironmentName = "NINJA_AGENT_NODE_ID"; PropertyName = "ninjaNodeId" },
|
||||||
|
@{ EnvironmentName = "NINJA_LOCATION_ID"; PropertyName = "ninjaLocationId" },
|
||||||
|
@{ EnvironmentName = "NINJA_LOCATION_NAME"; PropertyName = "ninjaLocationName" }
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($mapping in $mappings) {
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($mapping.EnvironmentName, "Process"))) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = [string]$clientConfiguration.($mapping.PropertyName)
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($value)) {
|
||||||
|
[Environment]::SetEnvironmentVariable($mapping.EnvironmentName, $value, "Process")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Warning "Could not restore stored NinjaOne context: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$arguments = @(
|
$arguments = @(
|
||||||
$dllPath
|
$dllPath
|
||||||
)
|
)
|
||||||
@@ -53,6 +87,8 @@ else {
|
|||||||
$secretFullPath = if ([string]::IsNullOrWhiteSpace($SecretPath)) { "" } else { Resolve-PathLike -PathValue $SecretPath -BasePath $repoRoot }
|
$secretFullPath = if ([string]::IsNullOrWhiteSpace($SecretPath)) { "" } else { Resolve-PathLike -PathValue $SecretPath -BasePath $repoRoot }
|
||||||
$canUpload = (Test-Path $clientConfigFullPath) -and (-not [string]::IsNullOrWhiteSpace($secretFullPath)) -and (Test-Path $secretFullPath)
|
$canUpload = (Test-Path $clientConfigFullPath) -and (-not [string]::IsNullOrWhiteSpace($secretFullPath)) -and (Test-Path $secretFullPath)
|
||||||
|
|
||||||
|
Restore-NinjaContextFromClientConfiguration -Path $clientConfigFullPath
|
||||||
|
|
||||||
if ($UploadMode -eq "required" -and -not $canUpload) {
|
if ($UploadMode -eq "required" -and -not $canUpload) {
|
||||||
throw "UploadMode 'required' was set, but client config or protected secret is missing."
|
throw "UploadMode 'required' was set, but client config or protected secret is missing."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,10 @@
|
|||||||
<RootNamespace>OCSentinelCli</RootNamespace>
|
<RootNamespace>OCSentinelCli</RootNamespace>
|
||||||
<Product>OfficeCom Sentinel</Product>
|
<Product>OfficeCom Sentinel</Product>
|
||||||
<Company>OfficeCom</Company>
|
<Company>OfficeCom</Company>
|
||||||
<Version>1.3.4</Version>
|
<Version>1.3.6</Version>
|
||||||
<AssemblyVersion>1.3.4.0</AssemblyVersion>
|
<AssemblyVersion>1.3.6.0</AssemblyVersion>
|
||||||
<FileVersion>1.3.4.0</FileVersion>
|
<FileVersion>1.3.6.0</FileVersion>
|
||||||
<InformationalVersion>1.3.4</InformationalVersion>
|
<InformationalVersion>1.3.6</InformationalVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
Reference in New Issue
Block a user