From d15bb6539b45393f2e44ff84afd4c13b59af954f Mon Sep 17 00:00:00 2001 From: Johannes Rest Date: Thu, 30 Jul 2026 15:50:42 +0200 Subject: [PATCH] Separate BizTalk collection from Checkmk agent --- Dokumentation.md | 815 +++++++++++------- README.md | 570 ++++++------ deployment/Install-BizTalkCheckmkPulse.ps1 | 171 ++++ deployment/Uninstall-BizTalkCheckmkPulse.ps1 | 30 + deployment/checkmk/biztalk_checkmk_pulse.cmd | 8 +- docs/ACC-WMI-SQL-Berechtigung.txt | 189 ++-- docs/CheckmkServices.md | 12 +- docs/ExampleOutput.md | 16 +- docs/Integration.md | 329 +++---- scripts/package-release.cmd | 5 +- src/BizTalkCheckmkPulse/App.config | 10 +- .../BizTalkCheckmkPulse.csproj | 2 + .../CheckmkLocalFormatter.cs | 19 +- src/BizTalkCheckmkPulse/FileLogger.cs | 150 ++++ src/BizTalkCheckmkPulse/Models.cs | 1 + src/BizTalkCheckmkPulse/MonitoringOptions.cs | 46 + src/BizTalkCheckmkPulse/Program.cs | 166 +++- .../Properties/AssemblyInfo.cs | 7 + src/BizTalkCheckmkPulse/SnapshotStore.cs | 353 ++++++++ .../SqlConnectivityProbe.cs | 10 +- src/BizTalkCheckmkPulse/WmiBizTalkProbe.cs | 13 +- tests/BizTalkCheckmkPulse.Tests/Program.cs | 117 +++ 22 files changed, 2051 insertions(+), 988 deletions(-) create mode 100644 deployment/Install-BizTalkCheckmkPulse.ps1 create mode 100644 deployment/Uninstall-BizTalkCheckmkPulse.ps1 create mode 100644 src/BizTalkCheckmkPulse/FileLogger.cs create mode 100644 src/BizTalkCheckmkPulse/SnapshotStore.cs diff --git a/Dokumentation.md b/Dokumentation.md index f376e75..9f39857 100644 --- a/Dokumentation.md +++ b/Dokumentation.md @@ -1,110 +1,180 @@ -# Dokumentation: BizTalk Checkmk Pulse +# Technische Dokumentation: BizTalk Checkmk Pulse -## Zielbild +## 1. Ziel und Randbedingungen -Ziel ist ein wartbares Monitoring fuer BizTalk Server 2020 in den Umgebungen `ACC`, `DEV`, `TST` und `PRD`. Jede Umgebung besitzt einen BizTalk Server 2020 und einen SQL Server. SQL Server wird mit dem Checkmk-eigenen MSSQL-Plugin ueberwacht; fuer BizTalk liefert dieses Projekt die fehlende fachliche und technische Laufzeitsicht. +Die Anwendung ueberwacht BizTalk Server 2020 auf Windows Server 2019 mit +Checkmk 2.4. Pro Umgebung (`ACC`, `DEV`, `TST`, `PRD`) wird lokal auf dem +BizTalk-Server gesammelt. Das Checkmk-MSSQL-Plugin bleibt fuer die allgemeine +SQL-Ueberwachung zustaendig; BizTalk Checkmk Pulse liefert die +BizTalk-spezifische Laufzeit- und Berechtigungssicht. -Das Monitoring soll: +Verbindliche Randbedingungen: -- auf jeder BizTalk-Maschine lokal laufen -- ohne PowerShell-Abhaengigkeit funktionieren -- keine BizTalk-DLLs im Build erzwingen -- Checkmk-2.4-kompatible Services und Metriken erzeugen -- service-discovery-freundlich und dashboard-tauglich sein -- bei Fehlern gueltige `UNKNOWN`-Services statt kaputter Agent-Ausgaben liefern +- Der Checkmk Windows Agent bleibt `LocalSystem`. +- `LocalSystem` erhaelt keine BizTalk-/SQL-Gruppenmitgliedschaft. +- Ein separates Dienstkonto oder bevorzugt gMSA sammelt minuetlich. +- Der Agentpfad fuehrt niemals WMI- oder SQL-Abfragen aus. +- Fehler muessen als gueltige Checkmk-`UNKNOWN`-Services sichtbar werden. +- Schreiben und Lesen duerfen nie einen halben Snapshot exponieren. +- Laufzeitcode benoetigt keine PowerShell und keine BizTalk-DLL-Referenz. +- Installation, ACL und Scheduled Task muessen reproduzierbar sein. -## Technische Bewertung +## 2. Ausloeser der Architekturanderung -### Option A: Checkmk Local Check mit C#/.NET Framework - -Bewertung: empfohlen und umgesetzt. - -Vorteile: - -- Checkmk 2.4 unterstuetzt Local Checks direkt. -- Windows Server mit BizTalk 2020 bringt .NET Framework in der Regel passend mit. -- `System.Management` kann BizTalk-WMI lesen. -- Keine PowerShell Execution Policy, keine Script-Signing-Frage. -- Kein serverseitiges Checkmk-Python-Plugin notwendig. -- Rollout ist eine einfache Dateiablage unter `%ProgramData%\checkmk\agent\local`. - -Nachteile: - -- Schwellwerte sind in der `.exe.config`, nicht als Checkmk-Regelsatz in WATO. -- Eigene Graphing-Definitionen sind nicht enthalten; Checkmk zeigt Local-Check-Metriken trotzdem als Performance-Daten und Graphen. - -### Option B: Agent Plugin plus serverseitiges Checkmk-Plugin - -Bewertung: technisch elegant, aber fuer den ersten produktiven Schritt schwerer. - -Vorteile: - -- Checkmk-Regeln, Discovery und Metrikdefinitionen koennen sauber zentral modelliert werden. -- Bessere langfristige Erweiterbarkeit als MKP. - -Nachteile: - -- Checkmk-Check-API-Versionen muessen enger gepflegt werden. -- Server-seitige Installation in jeder Site erforderlich. -- Mehr Aufwand fuer Managed-Services-Betrieb und Updates. - -Empfehlung: als Version 2 dieses Projekts denkbar, wenn die Local-Check-Variante stabil in PRD laeuft und zentrale Regelsaetze wirklich benoetigt werden. - -### Option C: PowerShell Local Check - -Bewertung: nicht empfohlen fuer diese Umgebung. - -Vorteile: - -- Schnell zu schreiben. -- WMI/CIM-Zugriff ist komfortabel. - -Nachteile: - -- PowerShell ist in vielen Serverumgebungen eingeschraenkt oder signaturpflichtig. -- Ausfuehrungsverhalten im Checkmk-Agent-Kontext ist haeufiger fehleranfaellig. - -### Option D: BizTalk ExplorerOM/OperationsOM - -Bewertung: fachlich stark, deployseitig unnoetig schwer. - -Vorteile: - -- Hoehere BizTalk-Abstraktion als rohe WMI-Klassen. - -Nachteile: - -- BizTalk-DLL-Versionen muessen beim Build und teilweise zur Laufzeit passen. -- Build-Agenten brauchen BizTalk-Komponenten oder SDK-Dateien. -- Fuer die benoetigten Zustandsdaten reicht WMI aus. - -## Architektur +Der ACC-Agent-Dump vom 29.07.2026 bewies: ```text -Checkmk Windows Agent - | - | startet lokale Checks aus %ProgramData%\checkmk\agent\local - v -biztalk_checkmk_pulse.cmd - | - | startet - v -BizTalkCheckmkPulse.exe - | - | liest lokal und prueft im gleichen Sicherheitskontext - +-- WMI root\MicrosoftBizTalkServer - +-- Windows Application Event Log - +-- SQL-Verbindung zu BizTalkMgmtDb/BizTalkMsgBoxDb - | - v -Checkmk Local Check Zeilen nach STDOUT +Lokaler Prozess: NT AUTHORITY\SYSTEM +Netzwerkprincipal: BEW\AV23AGPWBIO1$ +Providerfehler: COMException 0x80131904 +Innerer Fehler: Login failed for user 'BEW\AV23AGPWBIO1$' ``` -Der `.cmd`-Wrapper liefert auch dann fuer alle sechs stabilen Services gueltige `UNKNOWN`-Zeilen mit Massnahme, wenn die EXE fehlt oder bereits der Prozessstart mit einem Exitcode fehlschlaegt. Die EXE selbst faengt Laufzeitfehler ab und schreibt ebenfalls fuer alle stabilen Services `UNKNOWN`, damit unvollstaendige Deployments oder WMI-Probleme in Checkmk sichtbar bleiben. +Lokales `root\MicrosoftBizTalkServer` war erreichbar. Die +datenbankgestuetzten BizTalk-WMI-Klassen scheiterten erst beim Zugriff des +Providers auf den getrennten SQL Server. DCOM-, Firewall- oder pauschale +WMI-ACL-Aenderungen beheben diesen Befund nicht. -## Datenquellen +Eine Berechtigung des Maschinenkontos haette die Netzwerkrolle allen +`LocalSystem`-Diensten des BizTalk-Servers vermittelt. Die jetzt implementierte +Producer/Consumer-Trennung beschraenkt sie stattdessen auf ein dediziertes +Konto. -### BizTalk WMI Namespace +## 3. Architektur + +```text + Privilegierte Zone +┌──────────────────────────────────────────────────────────────┐ +│ Task Scheduler: "BizTalk Checkmk Pulse Provider" │ +│ Konto: DOMAIN\svc_biztalk_cmk$ (gMSA empfohlen) │ +│ Intervall: 1 Minute, IgnoreNew, Laufzeitlimit 5 Minuten │ +│ │ +│ BizTalkCheckmkPulse.exe --collect │ +│ ├─ WMI root\MicrosoftBizTalkServer │ +│ ├─ integrierter SQL-Test an MgmtDb/MsgBoxDb │ +│ ├─ lokales Windows Application Event Log │ +│ ├─ Checkmk-Formatierung │ +│ └─ atomare Snapshot-Publikation + Provider-Log │ +└──────────────────────────────┬───────────────────────────────┘ + │ Datei/ACL + v + %ProgramData%\BizTalkCheckmkPulse\data\biztalk-checkmk-pulse.snapshot + │ + v +┌──────────────────────────────────────────────────────────────┐ +│ Unprivilegierter Transportpfad │ +│ Checkmk-Agent (LocalSystem) │ +│ └─ local\biztalk_checkmk_pulse.cmd │ +│ └─ BizTalkCheckmkPulse.exe --consume │ +│ ├─ Format/Alter/Maschine/SHA-256 validieren │ +│ └─ Payload unveraendert nach STDOUT │ +└──────────────────────────────────────────────────────────────┘ +``` + +Eine gemeinsame EXE implementiert beide Modi. Das reduziert +Versionsabweichungen. Die zentrale Config unter `%ProgramFiles%` gilt fuer +Provider und Consumer. + +### 3.1 Provider-Modus + +Aufruf: + +```cmd +"%ProgramFiles%\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe" --collect +``` + +Ablauf: + +1. Config laden und absolute Runtimepfade validieren. +2. Tageslogs gemaess `LogRetentionDays` bereinigen. +3. exklusives Handle auf `.provider.lock` halten. +4. WMI-, SQL- und Event-Log-Probes ausfuehren. +5. sechs stabile und optionale dynamische Checkmk-Zeilen formatieren. +6. Snapshot in einer eindeutigen Temporaerdatei desselben Verzeichnisses + schreiben. +7. `Flush(true)` ausfuehren und Temporaerdatei atomar publizieren. +8. Laufzeit, Zeilen- und Diagnosezahl loggen. + +Exitcodes: + +| Code | Bedeutung | +| --- | --- | +| `0` | Sammlung und Snapshot-Publikation erfolgreich. | +| `1` | unerwarteter Providerfehler; aktueller `UNKNOWN`-Snapshot wird versucht. | +| `2` | Parallelstart oder Snapshot-I/O-Fehler. | + +Fachliche WMI-/SQL-Fehler werden strukturiert in den Snapshot geschrieben und +muessen nicht zwingend einen Prozessfehler verursachen. So bleibt die genaue +Fehlerkategorie in Checkmk sichtbar. + +### 3.2 Consumer-Modus + +Aufruf: + +```cmd +"%ProgramFiles%\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe" --consume +``` + +Ohne Modusargument ist `--consume` der sichere Default. Der Checkmk-Wrapper +setzt es trotzdem explizit. + +Der Consumer: + +- oeffnet nur den Snapshot, +- begrenzt die Dateigroesse vor dem Einlesen, +- validiert striktes UTF-8, +- validiert Formatversion und Header, +- fordert die lokale Maschinenkennung, +- berechnet SHA-256 ueber den exakten Payload, +- prueft Zeilenanzahl und Checkmk-Zeilenform, +- verwirft Zeitstempel mehr als fuenf Minuten in der Zukunft, +- verwirft Snapshots nach `SnapshotMaxAgeSeconds`, +- gibt bei Erfolg ausschliesslich den validierten Payload aus. + +Er gibt immer Exitcode `0` zurueck, damit ein Fehler die komplette +Checkmk-Agentsektion nicht zerstoert. Jede Ablehnung erzeugt sechs +`UNKNOWN`-Zeilen und einen Eintrag im Consumer-Log. + +## 4. Snapshot-Vertrag + +Version 1: + +```text +BIZTALK_CHECKMK_PULSE_SNAPSHOT_V1 +generatedUtc=2026-07-30T12:34:56.1234567Z +machineBase64=QVYyM0FHUFdCSU8x +identityBase64=QkVXXHN2Y19iaXp0YWxrX2NtayQ= +payloadLines=6 +payloadSha256=<64 hex characters> + +0 "BizTalk Platform" - ... +... +``` + +Maschine und Identitaet sind Base64-kodiert, um Zeilenumbrueche und +Trennzeichen im Header auszuschliessen. Base64 ist hier keine Verschluesselung. +Die SHA-256-Pruefung schuetzt vor unbemerkter Teil-/Fehlschreibung, nicht vor +einem lokalen Administrator. + +### 4.1 Atomare Publikation + +Die Temporaerdatei liegt zwingend im Snapshotverzeichnis. Dadurch findet der +Ersatz auf demselben Volume statt. Existiert bereits ein Snapshot, wird +`File.Replace` verwendet; beim ersten Lauf `File.Move`. Bis zum erfolgreichen +Ersatz bleibt der letzte vollstaendige Snapshot erhalten. Seine Alterspruefung +verhindert unbegrenztes Weiterreichen alter Daten. + +### 4.2 Keine direkte Checkmk-Spooldatei + +Checkmk kann zwar Dateien aus seinem Spoolverzeichnis direkt anhaengen, prueft +deren Inhalt aber laut eigener Dokumentation nicht. Ein fehlerhafter Spool kann +nachfolgende Agentsektionen beeinflussen. Deshalb liegt der Provider-Snapshot +in einem eigenen Verzeichnis und wird erst durch den validierenden Consumer in +die Local-Check-Ausgabe uebernommen. + +## 5. Datenquellen + +### 5.1 BizTalk WMI Namespace: @@ -112,286 +182,361 @@ Namespace: root\MicrosoftBizTalkServer ``` -Genutzte Klassen: +Pflichtklassen: | Klasse | Zweck | | --- | --- | -| `MSBTS_GroupSetting` | BizTalk-Gruppe, Management-DB und Master-MessageBox (`SubscriptionDB*`). | -| `MSBTS_HostInstance` | Host-Instance-Zustand; Ergebnis wird clientseitig auf den ueberwachten Server begrenzt. | -| `MSBTS_ServiceInstance` | Suspended service instances. | -| `MSBTS_ReceiveLocation` | Receive-Location-Zustand. | -| `MSBTS_SendPort` | Send-Port-Zustand. | -| `MSBTS_Orchestration` | Orchestration-Zustand. | -| `MSBTS_ReceivePort` | Optionales Best-Effort-Application-Mapping, nur wenn per-Application-Services aktiviert sind. | +| `MSBTS_GroupSetting` | Gruppe, Read-Only-/Operator-Gruppe, Management- und Master-MessageBox-Ziel. | +| `MSBTS_HostInstance` | lokale Host-Instance-Zustaende. | +| `MSBTS_ServiceInstance` | suspendierte Instanzen. | +| `MSBTS_ReceiveLocation` | Receive-Location-Zustaende. | +| `MSBTS_SendPort` | Send-Port-Zustaende. | +| `MSBTS_Orchestration` | Orchestration-Zustaende. | -Die Plattformabfrage verwendet absichtlich keine vermeintliche Klasse `MSBTS_MessageBoxSetting`: Sie ist nicht Bestandteil des dokumentierten BizTalk-WMI-Schemas. Auch die vorhandene Klasse `MSBTS_MsgBoxSetting` ist fuer die Zielermittlung nicht erforderlich. `MSBTS_GroupSetting` liefert mit `SubscriptionDBServerName` und `SubscriptionDBName` bereits das Ziel der Master-MessageBox. Dadurch entfallen eine Providerabfrage und eine unnoetige Berechtigungs-/Schemaschnittstelle. +`MSBTS_GroupSetting` wird mit den dokumentierten Properties +`MgmtDbServerName`, `MgmtDbName`, `SubscriptionDBServerName`, +`SubscriptionDBName`, `BizTalkReadOnlyUserGroup` und +`BizTalkOperatorGroup` gelesen. Eine nicht dokumentierte +`MSBTS_MessageBoxSetting`-Klasse wird nicht abgefragt. -### Windows Application Event Log +Hostnamen werden clientseitig verglichen. Sonderzeichen und FQDN-/Kurzname +gelangen nicht in dynamisch erzeugte WQL-Filter. -Der Check liest standardmaessig das lokale Application Log fuer die letzten 60 Minuten und filtert auf Quellen wie: +### 5.2 SQL-Zugriffsprobe -- `BizTalk Server` -- `XLANG/s` -- `ENTSSO` -- `BizTalk Server Application` -- `BizTalk Server EDI` +Der Provider oeffnet fuer Management- und Master-MessageBox-Datenbank eine +kurze `System.Data.SqlClient`-Verbindung mit integrierter +Windows-Authentifizierung, fuehrt `SELECT 1` aus und schliesst sie. Keine +BizTalk-Tabelle wird direkt gelesen oder veraendert. -Die Liste ist ueber `EventLogSources` konfigurierbar. +Die Ausgabe `execution_identity=` zeigt deshalb das Provider-Konto, nicht mehr +`NT AUTHORITY\SYSTEM`. Der Check beweist, dass genau das Scheduled-Task-Konto +die Ziele erreichen kann. -### SQL-Zugriffsprobe +### 5.3 Event Log -Nach erfolgreicher oder teilweise erfolgreicher Plattformabfrage uebernimmt der SQL-Probe die per `MSBTS_GroupSetting` ermittelten Management- und Master-MessageBox-Ziele. Fuer jedes eindeutige Ziel wird mit `System.Data.SqlClient` eine Verbindung mit integrierter Windows-Authentifizierung geoeffnet und `SELECT 1` ausgefuehrt. Die Verbindung wird unmittelbar danach geschlossen; es werden keine BizTalk-Tabellen gelesen oder veraendert. +Der Provider liest das lokale Application Log im konfigurierten Zeitfenster +und filtert Quellen wie `BizTalk Server`, `XLANG/s`, `ENTSSO`, +`BizTalk Server Application` und `BizTalk Server EDI`. -Der Test laeuft unter derselben Identitaet wie der Checkmk Local Check. Damit wird sichtbar, ob `LocalSystem` beziehungsweise das Maschinenkonto des BizTalk-Servers das SQL-Ziel tatsaechlich erreichen und die Datenbank oeffnen kann. Die Probe ist mit `ProbeSqlConnectivity=false` deaktivierbar und verwendet `SqlConnectionTimeoutSeconds` mit dem Default 5 Sekunden je Ziel als Timeout. +## 6. Berechtigungsmodell -Im produktiven Betrieb sollte der komplette Local Check asynchron mit 300 Sekunden Cache ausgefuehrt werden. So fuehren fehlende SQL-Rechte nicht bei jedem Checkmk-Abruf zu weiteren fehlgeschlagenen Login-Ereignissen. Der Trade-off ist eine Zustandsverzoegerung von maximal fuenf Minuten. +### 6.1 Bevorzugte BizTalk-Rolle -## Berechtigungsmodell +BizTalk Server 2020 fuehrte die `BizTalk Server Read Only Users`-Gruppe ein. +Sie darf Artefakte, Servicezustand, Message Flow und Trackinginformationen +lesen, aber keine administrativen Operationen ausfuehren. SQL-seitig wird sie +ueber `BTS_READONLY_USERS` vermittelt. -### LocalSystem und lokaler WMI-Zugriff - -Der Checkmk Windows Agent und der Agent Controller laufen standardmaessig als `LocalSystem` (`NT AUTHORITY\SYSTEM`). Der Wrapper und die EXE erben diesen Kontext. Das Plugin verbindet sich lokal mit `\\\root\MicrosoftBizTalkServer`, setzt keine eigenen Anmeldedaten, nutzt kein Remote-WMI und fuehrt keine veraendernden WMI-Methoden aus. Das lokale Windows Application Event Log wird ebenfalls nur gelesen. - -Die lokalen Rechte von `LocalSystem` reichen fuer diese Zugriffe normalerweise aus. Im regulaeren lokalen Betrieb werden deshalb keine zusaetzlichen DCOM-, Firewall- oder pauschalen WMI-Namespace-Freigaben benoetigt. - -### Netzwerkidentitaet zum SQL Server - -BizTalk-WMI-Klassen koennen ihre Daten aus der BizTalk Management- oder MessageBox-Datenbank beziehen. Liegt SQL Server auf einer anderen Maschine, authentifiziert sich `LocalSystem` dort mit dem Active-Directory-Computerkonto des BizTalk-Servers: +Soll: ```text -DOMAIN\BIZTALKSERVER$ +DOMAIN\svc_biztalk_cmk$ + -> konfigurierte BizTalk Server Read Only Users-Domaingruppe + -> vorhandener Windows-Gruppenlogin am SQL Server + -> BTS_READONLY_USERS in den von BizTalk konfigurierten Datenbanken ``` -Dieses Konto besitzt nicht automatisch BizTalk- oder SQL-Berechtigungen. Daraus kann die Situation entstehen, dass die Verbindung zum lokalen WMI-Namespace erfolgreich ist, einzelne SQL-gestuetzte WMI-Klassen aber `Access denied`, `UnauthorizedAccessException` oder `UNKNOWN` liefern. +Die genaue Gruppe ist aus +`MSBTS_GroupSetting.BizTalkReadOnlyUserGroup` beziehungsweise der +BizTalk-Administration zu lesen. Nicht vom Standardnamen ausgehen. -Der ACC-Test vom 29.07.2026 bestaetigt genau diesen Pfad: Auf `AV23AGPWBIO1` ist WMI erreichbar, aber der Provider meldet `COMException 0x80131904` mit `Login failed for user 'BEW\AV23AGPWBIO1$'`. Die `UNKNOWN`-Services fuer Plattform, Host Instances, Runtime Artifacts und Suspensions sowie die fehlende SQL-Zielermittlung sind Folgefehler. DCOM-, Firewall- oder WMI-ACL-Erweiterungen beheben diesen konkreten SQL-Loginfehler nicht. Der Event-Log-Check arbeitet bereits und seine Fehler/Warnungen sind separat zu bewerten. +Die Operator-Gruppe (`BTS_OPERATORS`) wird erst geprueft, wenn eine konkret +benannte erforderliche WMI-Klasse trotz nachgewiesener Read-Only-Konfiguration +und neuem Anmeldetoken abgewiesen wird. Die Erweiterung muss mit Security, +BizTalk- und SQL-Administration begruendet werden. -Die Codekorrektur ersetzt die Berechtigungsfreigabe nicht. Sie klassifiziert den Fehler korrekt, zeigt das abgewiesene Konto und fuehrt die SQL-Folgediagnose als Berechtigungsfehler. Sie veraendert weder AD noch SQL Server. Daher bleibt die neue Version ohne Gruppenfreigabe `UNKNOWN`; umgekehrt kann auch die alte Version nach korrekter Freigabe grundsaetzlich auf die Daten zugreifen. +Unzulaessig: -### Pruefung und Freigabe +- Checkmk-Dienstkonto aendern, +- Maschinenkonto pauschal berechtigen, +- individueller SQL-Login fuer das Provider-Konto, +- direkte ad-hoc DB-Rollen, +- `db_owner` oder `sysadmin`, +- WMI-/DCOM-Vollzugriff als Reaktion auf einen SQL-Loginfehler. -Der direkte Programmstart in einer administrativen Shell laeuft unter dem angemeldeten Benutzer und ist deshalb kein ausreichender Berechtigungstest. Verbindlich ist die Ausfuehrung durch den Checkmk Agent Controller als `LocalSystem`: +### 6.2 Dateisystem-ACL + +Der Installer entfernt die Vererbung an den Runtimeverzeichnissen und setzt +explizite Eintraege mit sprachneutralen Well-known-SIDs: + +| Ziel | Collector | SYSTEM | lokale Administratoren | +| --- | --- | --- | --- | +| Runtime-Root | RX | RX | F | +| `data` | Modify | RX | F | +| `logs` | Modify | Modify | F | + +`LocalSystem` kann deshalb den Snapshot lesen, aber im Datenverzeichnis nicht +schreiben. Schreibrecht im Logverzeichnis wird benoetigt, um abgelehnte +Snapshots aus dem Consumer-Kontext zu protokollieren. + +## 7. Scheduled Task + +Name: + +```text +BizTalk Checkmk Pulse Provider +``` + +Sollkonfiguration: + +| Einstellung | Wert | +| --- | --- | +| Trigger | einmalig, Wiederholung jede Minute ohne Enddatum | +| Programm | `%ProgramFiles%\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe` | +| Argument | `--collect` | +| Arbeitsverzeichnis | `%ProgramFiles%\BizTalkCheckmkPulse` | +| Benutzer | dediziertes Dienstkonto oder gMSA | +| Mehrfachinstanzen | `IgnoreNew` | +| Laufzeitlimit | 5 Minuten | +| StartWhenAvailable | aktiv | +| Restart | zweimal im Minutenabstand | + +Der Task wird mit gespeichertem Dienstkontokennwort beziehungsweise gMSA +ausgefuehrt, also unabhaengig von einer interaktiven Anmeldung. + +## 8. Installation + +### 8.1 Build + +```cmd +scripts\test-release.cmd +scripts\package-release.cmd +``` + +### 8.2 gMSA + +Voraussetzungen: + +1. gMSA in AD erstellen und Abrufrecht auf den BizTalk-Server begrenzen. +2. Konto lokal installieren und mit `Test-ADServiceAccount` pruefen. +3. gMSA in die exakt konfigurierte BizTalk-Read-Only-Gruppe aufnehmen. +4. AD-Replikation abwarten. + +Installation: ```powershell -& "C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe" dump | - Select-String -Pattern "BizTalk|Access denied|Unauthorized|UNKNOWN" -Context 0,1 +.\Install-BizTalkCheckmkPulse.ps1 ` + -CollectorAccount 'BEW\svc_biztalk_cmk$' ` + -Gmsa ` + -EnvironmentName ACC ``` -Bei Zugriffsfehlern gilt folgendes Least-Privilege-Vorgehen: +### 8.3 regulaeres Dienstkonto -1. Die exakt konfigurierte Operator-Gruppe in der BizTalk Administration Console unter `BizTalk Group` > `Properties` > `General` feststellen. -2. Das Computerobjekt des BizTalk-Servers durch einen AD-Administrator in genau diese Gruppe aufnehmen. Fuer ACC ist dies `AV23AGPWBIO1` beziehungsweise `BEW\AV23AGPWBIO1$`. -3. Keine direkten SQL-Logins, BizTalk-Datenbankrollen oder `sysadmin`-Rechte fuer das Maschinenkonto anlegen. -4. AD-Replikation abwarten und den Server im Wartungsfenster neu starten; alternativ Maschinen-Tickets mit `klist purge -li 0x3e7` und den Checkmk-Dienst erneuern. -5. Den Agent-Dump wiederholen und `operator_group`, zwei SQL-Ziele sowie das Verschwinden der Berechtigungs-`UNKNOWN`s kontrollieren. -6. Nur fuer weiterhin abgelehnte, konkret identifizierte WMI-Klassen mit BizTalk- und SQL-Administration pruefen, ob Administratorrechte erforderlich sind. +```powershell +.\Install-BizTalkCheckmkPulse.ps1 ` + -CollectorAccount 'BEW\svc_biztalk_cmk' ` + -EnvironmentName ACC +``` -Die Operator-Rolle ist fuer grundlegendes Monitoring und Zustandsabfragen vorgesehen. Direkte manuelle Aenderungen an den Rollen der BizTalk-SQL-Datenbanken sind zu vermeiden; die durch BizTalk konfigurierte Windows-Gruppe ist die vorgesehene Berechtigungsgrenze. +Der Installer muss als lokaler Administrator laufen. Er vergibt keine +AD-/BizTalk-/SQL-Rechte; diese bleiben getrennte administrative Freigaben. -Im Normalfall ist diese Windows-Gruppe bereits als SQL-Gruppenlogin und als Datenbankbenutzer eingerichtet. Ihre Benutzerzuordnung vermittelt `BTS_OPERATORS` unter anderem in `BizTalkMgmtDb` und `BizTalkMsgBoxDb`. `BEW\AV23AGPWBIO1$` erhaelt den Zugriff durch die AD-Gruppenmitgliedschaft und braucht keinen eigenen SQL-Login. +### 8.4 Checkmk -Bleibt der Fehler trotz bestaetigter Gruppenmitgliedschaft, AD-Replikation und erneuertem Maschinen-Token bestehen, pruefen BizTalk- und SQL-Administration: +Der installierte Wrapper liegt unter: -1. Ist die exakt konfigurierte Domain-Gruppe in `sys.server_principals` als Windows-Gruppe vorhanden? -2. Existiert ihr Datenbankbenutzer in `BizTalkMgmtDb` und `BizTalkMsgBoxDb`? -3. Ist dieser Benutzer in beiden Datenbanken Mitglied von `BTS_OPERATORS`? -4. Entspricht diese Abbildung weiterhin der BizTalk-Konfiguration? +```text +%ProgramData%\checkmk\agent\local\biztalk_checkmk_pulse.cmd +``` -Fehlende Zuordnungen werden fuer die konfigurierte Gruppe konsistent repariert. Ein individueller Login, manuelle Sonderrollen oder `sysadmin` fuer das Maschinenkonto waeren keine geeignete Abkuerzung. +Der Agent darf den Consumer synchron ausfuehren. Er liest nur maximal 1 MiB +lokale Daten; ein Checkmk-Async-Cache ist daher nicht notwendig. Falls alte +`check_mk.user.yml`-Regeln fuer den bisherigen 300-Sekunden-Cache existieren, +sollten sie entfernt werden, damit Checkmk den minuetlich aktualisierten +Snapshot nicht zusaetzlich verzoegert. -| Agent-Dump-Ergebnis | Massnahme | -| --- | --- | -| Plausible Werte fuer alle BizTalk-Services | Keine Berechtigungsaenderung. | -| WMI-Namespace nicht erreichbar | BizTalk-WMI-Provider, WMI-Dienst, Namespace und ACL gezielt pruefen. | -| Nur SQL-gestuetzte Klassen scheitern | Computerkonto in die BizTalk-Operator-Gruppe aufnehmen. | -| Nur Event-Log-Service ist `UNKNOWN` | Lokalen Application-Log-Zugriff pruefen. | -| Fehler bleibt mit Operator-Rolle bestehen | Betroffene Klasse und konkrete BizTalk-/SQL-Rollenanforderung untersuchen. | +Nach dem Agent-Dump: -### Automatische Diagnose im Programm +1. Service Discovery fuer den BizTalk-Host ausfuehren. +2. sechs stabile Services aufnehmen. +3. Changes aktivieren. +4. Views/Benachrichtigungen nach Umgebung konfigurieren. -WMI-Verbindungsaufbau und jede erforderliche WMI-Klasse werden separat bewertet. Erwartbare Exceptions werden in folgende Kategorien eingeordnet: +## 9. Logging -| Diagnose | Bedeutung | Ausgegebene Massnahme | +Pfad: + +```text +%ProgramData%\BizTalkCheckmkPulse\logs\ + biztalk-checkmk-pulse-YYYYMMDD.log +``` + +Jede Zeile enthaelt UTC-Zeit, Level, Komponente, PID, Windows-Identitaet und +einzeilige Nachricht. Provider-Erfolge werden pro Lauf geloggt; der Consumer +loggt nur abgelehnte Snapshots. Exceptions werden mit Typ, Nachricht, +HRESULT-/Providerdetails und Stacktrace einzeilig gespeichert. + +Logging ist best effort: Ein blockiertes Log darf Checkmk-Ausgabe oder +Snapshot-Publikation nicht zerstoeren. Der Provider entfernt beim Start Dateien +aelter als `LogRetentionDays`. + +## 10. Diagnose und Runbook + +### 10.1 Grundpruefung + +```powershell +Get-ScheduledTaskInfo -TaskName 'BizTalk Checkmk Pulse Provider' +Get-Item "$env:ProgramData\BizTalkCheckmkPulse\data\biztalk-checkmk-pulse.snapshot" +Get-Content "$env:ProgramData\BizTalkCheckmkPulse\logs\*.log" -Tail 100 +& "$env:ProgramFiles\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe" --consume +& "C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe" dump +``` + +### 10.2 Kein Snapshot + +Pruefen: + +1. Task existiert und ist aktiviert. +2. `LastTaskResult` und Provider-Log. +3. Dienstkonto kann sich als Batch anmelden. +4. gMSA ist lokal installiert und abrufbar. +5. Provider besitzt Modify auf `data` und `logs`. +6. EXE/Config sind ausfuehrbar. + +### 10.3 Staler Snapshot + +Der Consumer nennt Alter und Grenzwert. Ursachen: + +- Task deaktiviert oder Konto/Kennwort ungueltig, +- vorheriger Lauf haengt, +- WMI-/SQL-Timeouts, +- Task Scheduler lief waehrend Wartung nicht, +- Snapshot-Publikation wird durch AV/EDR oder ACL blockiert. + +Nicht einfach `SnapshotMaxAgeSeconds` stark erhoehen. Zuerst den minuetlichen +Lauf wiederherstellen. Bei geplanten Wartungen darf Checkmk bewusst `UNKNOWN` +anzeigen. + +### 10.4 SHA-256-/Formatfehler + +Snapshot nicht reparieren oder direkt an Checkmk kopieren. Task neu starten, +Datentraeger und AV/EDR pruefen. Wiederholt sich der Fehler, Temporaerdateien, +NTFS-Ereignisse und Schreibrechte untersuchen. + +### 10.5 BizTalk-/SQL-Permission + +1. Log zeigt `execution_identity` des Providers. +2. `BizTalkReadOnlyUserGroup` gegen BizTalk-Konfiguration abgleichen. +3. AD-Gruppenmitgliedschaft des Provider-Kontos pruefen. +4. neuen Logontoken durch neuen Tasklauf sicherstellen. +5. SQL-seitig Gruppenlogin und `BTS_READONLY_USERS` read-only pruefen. +6. keine Einzelrechte erteilen. +7. Nur wenn eine spezifische Klasse weiter abgewiesen wird, Read-Only-Umfang + mit Microsoft-Dokumentation und BizTalk-Betrieb abgleichen; Operator-Rolle + ist eine bewusste Eskalation. + +### 10.6 WMI-Schema + +`InvalidClass` oder `InvalidQuery` wird als `Wmi/Schema` klassifiziert. +Berechtigungserhoehung kann eine nicht vorhandene Klasse nicht erzeugen. +Installierte BizTalk-Version, WQL und Properties pruefen. + +## 11. Konfiguration + +Alle AppSettings befinden sich in: + +```text +%ProgramFiles%\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe.config +``` + +Runtime: + +| Key | Default | Bereich | | --- | --- | --- | -| `Wmi/Permission` | Namespace, BizTalk-WMI-Klasse oder deren eingebetteter SQL-Zugriff verweigert den Zugriff. | Namespace-ACL nur bei Verbindungsfehlern; bei `Login failed for user` das genannte Maschinenkonto der BizTalk-Operator-Gruppe zuordnen. | -| `Wmi/Connectivity` | WMI-/RPC-Ziel nicht erreichbar. | WMI-Dienst, Provider und bei Remote-WMI zusaetzlich DNS/RPC/Firewall pruefen. | -| `Wmi/Timeout` | WMI-Abfrage ueberschreitet `QueryTimeoutSeconds`. | WMI-, BizTalk- und SQL-Auslastung untersuchen, bevor der Timeout erhoeht wird. | -| `Wmi/Configuration` | BizTalk-WMI-Namespace fehlt oder Plattformdaten sind unvollstaendig. | Provider, Namespace und BizTalk-Konfiguration pruefen. | -| `Wmi/Schema` | `InvalidClass` oder `InvalidQuery`; Klasse beziehungsweise WQL passt nicht zum installierten Provider. | Klasse/Properties gegen das BizTalk-WMI-Schema pruefen; keine Rechteerhoehung vornehmen. | -| `Sql/Permission` | Login oder Datenbankzugriff wird abgelehnt. | Maschinenkonto der BizTalk-Operator-Gruppe zuordnen, Kerberos erneuern, keine direkten DB-Rollen vergeben. | -| `Sql/Connectivity` | SQL-Server oder Instanz nicht erreichbar. | Servername, DNS, SQL-Dienst, TCP-Protokoll, Port und Firewall pruefen. | -| `Sql/Timeout` | SQL-Verbindung oder Testabfrage laeuft in den Timeout. | Netzwerk und SQL-Auslastung pruefen; Timeout nur begruendet anheben. | -| `Sql/Configuration` | WMI-Ziele unvollstaendig oder TLS-, Zertifikats-, SPN-/SSPI-Konfiguration fehlerhaft. | Plattformabfragen beziehungsweise Zertifikatskette, Verschluesselung, SPN und Kerberos gezielt pruefen. | +| `SnapshotPath` | `%ProgramData%\BizTalkCheckmkPulse\data\...` | absolut | +| `SnapshotMaxAgeSeconds` | `180` | 60 bis 86400 | +| `SnapshotMaxBytes` | `1048576` | 4096 bis 16777216 | +| `LogDirectory` | `%ProgramData%\BizTalkCheckmkPulse\logs` | absolut | +| `LogRetentionDays` | `30` | 1 bis 365 | -Jede Diagnose enthaelt Bereich/Kategorie, betroffene Komponente, eine kurze Ursache, `Massnahme:` und `Technik:` mit Exception-Typ, HRESULT oder SQL-Fehlernummer. Bei WMI-Abfragefehlern werden zusaetzlich WQL und Laufzeit bis zum Fehler ausgegeben. Erforderliche Datenquellen besitzen eigene Verfuegbarkeitsflags. Schlaegt beispielsweise `MSBTS_ServiceInstance` fehl, wird `BizTalk Suspended Instances` zwingend `UNKNOWN`; eine leere Ergebnisliste darf nicht als 'keine Suspensions' fehlinterpretiert werden. +BizTalk-Probes: -Die Operator-Mitgliedschaft des Computerkontos steht allen auf diesem Server als `LocalSystem` laufenden Diensten fuer Netzwerkzugriffe zur Verfuegung. Falls diese Sicherheitsauswirkung nicht akzeptabel ist, kann ein separater Collector unter einem dedizierten gMSA- oder Dienstkonto mit Operator-Rechten Checkmk-Spooldaten erzeugen. Diese Variante ist noch nicht Bestandteil der aktuellen Implementierung. Der komplette Checkmk-Agent sollte nicht allein fuer dieses Plugin auf eine andere Identitaet umgestellt werden, weil dies alle Agent-Sektionen und Local Checks betrifft. +| Key | Default | +| --- | --- | +| `Server` | `.` | +| `QueryTimeoutSeconds` | `25` | +| `ProbeSqlConnectivity` | `true` | +| `SqlConnectionTimeoutSeconds` | `5` | +| `ProbeEventLog` | `true` | +| `EventLogLookbackMinutes` | `60` | -Quellen: +Alarmierung: +| Key | Default | +| --- | --- | +| `WarnResumableThreshold` | `1` | +| `CritNonResumableThreshold` | `1` | +| `AlertOnArtifactRuntimeIssues` | `false` | +| `EmitPerApplicationSuspensionServices` | `false` | +| `EventLogWarnThreshold` | `1` | +| `EventLogCritThreshold` | `10` | + +## 12. Tests und Abnahmekriterien + +Automatisiert: + +- Release-Build .NET Framework 4.7.2, +- exakt sechs Self-Test-Services, +- unbekannte Quellen werden `UNKNOWN`, +- dynamische Anwendungsservices nur bei bekannter Anwendung, +- WMI-Queryvertrag nutzt dokumentierte Properties, +- eingebetteter SQL-Loginfehler wird `Permission`, +- abgewiesener Principal wird extrahiert, +- Snapshot-Roundtrip und Ersatz, +- SHA-256-Manipulation wird verworfen, +- Stale-Snapshot wird verworfen, +- Consumer-Fallback enthaelt sechs `UNKNOWN`-Services. + +Windows-/ACC-Abnahme: + +1. `LastTaskResult=0`. +2. Snapshot wird mindestens dreimal minuetlich aktualisiert. +3. Provider-Log nennt das dedizierte Konto. +4. `BizTalk Platform` zeigt `read_only_group=`. +5. `BizTalk SQL Access`: `targets=2`, `available=2`. +6. keine Permission-`UNKNOWN`s. +7. ACL-Test: `LocalSystem` kann Snapshot lesen, nicht schreiben. +8. Task deaktivieren: nach 180 Sekunden sechs stale-`UNKNOWN`s. +9. Task wieder aktivieren: naechster Snapshot stellt Echtzustand her. +10. Agent-Dump und Checkmk Service Discovery erfolgreich. + +Erst nach ACC-Abnahme erfolgt der gestufte Rollout nach DEV/TST/PRD. + +## 13. Deinstallation und Rollback + +```powershell +.\Uninstall-BizTalkCheckmkPulse.ps1 +``` + +`-KeepRuntimeData` erhaelt Logs und Snapshot. Das Skript entfernt Task, +Wrapper und Programmdateien, aber keine AD-Gruppenmitgliedschaft. Die +Berechtigung wird separat nach Change-Verfahren entzogen. + +Rollback auf die alte direkte `LocalSystem`-Architektur ist nicht empfohlen. +Falls kurzfristig notwendig, muss Security die Maschinenkonto-Berechtigung +explizit freigeben; der Checkmk-Agentdienst selbst darf nicht auf das +Collector-Konto umgestellt werden. + +## 14. Transport per certutil + +Die Source-Uebergabe besteht aus ZIP plus Base64-TXT: + +```cmd +certutil -decode .zip.b64.txt .zip +certutil -hashfile .zip SHA256 +tar -xf .zip +``` + +ZIP und Base64 werden erst nach dem Git-Commit aus genau diesem Commit erzeugt. + +## 15. Referenzen + +- https://learn.microsoft.com/en-us/biztalk/core/managing-biztalk-server-security +- https://learn.microsoft.com/en-us/biztalk/core/windows-groups-and-user-accounts-in-biztalk-server +- https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-groupsetting-biztalkreadonlyusergroup-property-wmi +- https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-groupsetting-wmi - https://docs.checkmk.com/latest/en/agent_windows.html - https://docs.checkmk.com/latest/en/localchecks.html -- https://learn.microsoft.com/en-us/biztalk/core/minimum-security-user-rights -- https://learn.microsoft.com/en-us/biztalk/core/access-control-and-data-security -- https://learn.microsoft.com/en-us/biztalk/core/windows-groups-and-user-accounts-in-biztalk-server -- https://learn.microsoft.com/en-us/biztalk/core/access-control-for-administrative-roles -- https://learn.microsoft.com/en-us/biztalk/core/how-to-modify-group-properties -- https://learn.microsoft.com/en-us/entra/architecture/service-accounts-computer -- https://learn.microsoft.com/en-us/powershell/module/activedirectory/add-adgroupmember -- https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/klist -- https://learn.microsoft.com/en-us/windows/win32/wmisdk/access-to-wmi-namespaces - -## Resilienz - -Das Plugin ist bewusst defensiv gebaut: - -- Der dependency-freie Regressionstest laeuft mit dem gleichen .NET-Framework-/MSBuild-Baseline wie die Anwendung. -- WMI-Queries haben ein konfigurierbares Timeout. -- WMI-Namespace und Pflichtklassen werden getrennt auf Berechtigung, Konfiguration, Erreichbarkeit und Timeout geprueft. -- Management- und Master-MessageBox-Datenbank werden aus einer dokumentierten WMI-Klasse ermittelt und mit der echten Agent-Identitaet getestet. -- `InvalidClass`/`InvalidQuery` werden als Schemafehler statt als Berechtigungsproblem ausgewiesen. -- Host Instances werden ohne namensabhaengigen WQL-Filter clientseitig auf den ueberwachten Server begrenzt. -- Bereits fuer Runtime-Zustaende gelesene Artefakte werden fuer das Best-Effort-Application-Mapping wiederverwendet; doppelte WMI-Abfragen entfallen. -- SQL-Fehlernummern werden in Berechtigung, Erreichbarkeit, Timeout oder Providerfehler klassifiziert. -- Fehlgeschlagene Pflichtabfragen erzeugen `UNKNOWN` statt irrefuehrender Nullwerte. -- Optionale WMI-Klassen erzeugen Diagnosehinweise statt Totalabbruch. -- Fehlende Properties werden als leer/unknown behandelt. -- Local-Check-Ausgaben verwenden gueltige Checkmk-Zeilen mit genau vier Feldern. -- Fatal Errors erzeugen `UNKNOWN` fuer alle stabilen Services. -- Event-Log-Auswertung ist isoliert; ein Fehler dort bricht WMI-Monitoring nicht ab. -- Service-Namen sind stabil, damit Service Discovery nicht bei jedem Lauf neue Services erzeugt. - -## Statusmodell - -### BizTalk Platform - -- `OK`: BizTalk-WMI erreichbar. -- `UNKNOWN`: WMI nicht erreichbar oder kompletter Programmfehler. - -Dieser Service ist der Integrationsindikator. Wenn er `UNKNOWN` ist, sind Berechtigungen, BizTalk-Installation oder WMI-Repository zu pruefen. - -### BizTalk SQL Access - -- `OK`: Datenbankziele vollstaendig ermittelt und integrierte Anmeldung an allen Zielen erfolgreich. -- `UNKNOWN`: Zielermittlung unvollstaendig, Anmeldung verweigert, SQL nicht erreichbar, Timeout oder Providerfehler. -- Bei `ProbeSqlConnectivity=false`: `OK` mit sichtbarem Hinweis, dass die Probe deaktiviert ist. - -Metriken: `biztalk_sql_targets_total`, `biztalk_sql_targets_available` und `biztalk_sql_targets_failed`. Die Ausgabe nennt ausserdem `execution_identity`, `network_identity` und den Zustand jedes getesteten Datenbankziels. - -### BizTalk Suspended Instances - -- `OK`: keine suspendierten Instanzen. -- `WARN`: mindestens `WarnResumableThreshold` resumable suspended instances. -- `CRIT`: mindestens `CritNonResumableThreshold` non-resumable suspended instances. -- `UNKNOWN`: Datenquelle nicht lesbar. - -### BizTalk Host Instances - -- `OK`: alle Host-Instanzen sind started. -- `WARN`: mindestens eine Host-Instanz ist pending. -- `CRIT`: mindestens eine Host-Instanz ist stopped oder unknown. -- `UNKNOWN`: keine Host-Instanzen gefunden oder Datenquelle nicht lesbar. - -### BizTalk Runtime Artifacts - -- `OK`: Artefakte lesbar, keine unbekannten Statuswerte. -- `WARN`: nur wenn `AlertOnArtifactRuntimeIssues=true` und deaktivierte/inaktive Artefakte vorhanden sind. -- `CRIT`: unbekannte Send-Port- oder Orchestration-Statuswerte. -- `UNKNOWN`: mindestens eine erforderliche WMI-Artefaktklasse nicht lesbar. - -Deaktivierte Receive Locations und gestoppte Ports koennen in BizTalk fachlich korrekt sein. Deshalb ist die Alarmierung hier standardmaessig informativ. - -### BizTalk Event Log - -- `OK`: keine relevanten Fehler/Warnungen ueber Schwellwert. -- `WARN`: Fehler oder Warnungen ab `EventLogWarnThreshold`. -- `CRIT`: Fehler ab `EventLogCritThreshold`. -- `UNKNOWN`: Event Log nicht lesbar. - -## Dashboard-Empfehlung - -Pro Umgebung sollte der BizTalk-Host in einem eigenen Host-Ordner oder Host-Tag fuer `ACC`, `DEV`, `TST`, `PRD` liegen. Im Dashboard eignen sich: - -- Service State Widgets fuer die sechs stabilen BizTalk-Services -- Graphen fuer `biztalk_sql_targets_failed`, `biztalk_suspended_total`, `biztalk_host_instances_stopped`, `biztalk_eventlog_errors` -- Hostgruppe/Ordner pro Umgebung -- Optional eine View gefiltert auf `Service starts with BizTalk` - -Empfohlene Reihenfolge im Dashboard: - -1. `BizTalk Platform` -2. `BizTalk SQL Access` -3. `BizTalk Suspended Instances` -4. `BizTalk Host Instances` -5. `BizTalk Event Log` -6. `BizTalk Runtime Artifacts` -7. SQL-Server-Services aus dem Checkmk-MSSQL-Plugin - -## Rollout-Vorgehen - -1. Build-Paket erzeugen. -2. In `DEV` auf dem BizTalk-Server installieren. -3. `--self-test` und normalen Lauf ausfuehren. -4. Agent-Dump pruefen. -5. Checkmk Discovery durchfuehren. -6. Eine Woche Messwerte und false positives beobachten. -7. Nach `TST` und `ACC` uebernehmen. -8. In `PRD` mit `AlertOnArtifactRuntimeIssues=false` starten. -9. Nach Betriebsfreigabe Schwellwerte feinjustieren. - -## Troubleshooting - -### Service bleibt UNKNOWN - -Pruefen: - -```cmd -"%ProgramData%\checkmk\agent\local\biztalk_checkmk_pulse.cmd" -``` - -Wenn WMI nicht erreichbar ist: - -```cmd -wmic /namespace:\\root\MicrosoftBizTalkServer path MSBTS_HostInstance get HostName,RunningServer,ServiceState -``` - -Der Checkmk Windows Agent laeuft normalerweise als LocalSystem. Daher immer auch den Agent-Dump verwenden: - -```cmd -"C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe" dump -``` - -Die Diagnose im Service-Text nach `Massnahme:` abarbeiten. Wichtige Kategorien: - -- `Wmi/Permission`: lokale Namespace-ACL oder BizTalk-Operator-Zuordnung pruefen. -- `Wmi/Schema`: Klasse/WQL gegen das installierte BizTalk-Schema pruefen; `InvalidClass` ist nicht durch zusaetzliche Rechte loesbar. -- `Sql/Permission`: Maschinenkonto `DOMAIN\BIZTALKSERVER$` und BizTalk-Operator-Gruppe pruefen. -- `Sql/Connectivity`: SQL-Server-/Instanzname, DNS, SQL-Dienst, TCP und Firewall pruefen. -- `Sql/Timeout`: SQL- und Netzwerkauslastung pruefen; Timeout nicht als erste Massnahme erhoehen. - -Der direkte Aufruf des Wrappers kann wegen des angemeldeten Administratorkontos ein anderes Ergebnis liefern als der Agent-Dump. Fuer die Freigabe ist immer der Agent-Dump massgeblich. - -### Keine Services in Discovery - -Pruefen: - -- Liegt `biztalk_checkmk_pulse.cmd` direkt unter `%ProgramData%\checkmk\agent\local`? -- Gibt der Wrapper direkt eine Zeile im Format `0 "Service" metric=value Details` aus? -- Wurde der Checkmk-Agent nach Policy-/Bakery-Aenderungen neu ausgerollt? - -### Runtime Artifacts zeigt deaktivierte Artefakte - -Das ist standardmaessig `OK`, damit gewollt deaktivierte BizTalk-Artefakte nicht alarmieren. Fuer strengere PRD-Standards: - -```xml - -``` - -## Weiterentwicklung - -Sinnvolle naechste Ausbaustufen: - -- MKP mit Agent-Bakery-Regel fuer zentrale Konfiguration. -- Optionales serverseitiges Check-Plugin nach Checkmk Check API V2. -- Custom Dashboard/View als Checkmk GUI Extension. -- Ergaenzung um MessageBox-Spool/Tracking-Daten, falls operativ benoetigt. +- https://docs.checkmk.com/latest/en/spool_directory.html diff --git a/README.md b/README.md index b39dbaf..12dd76b 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,80 @@ # BizTalk Checkmk Pulse -`BizTalk Checkmk Pulse` ist ein lokaler Checkmk-Check fuer Microsoft BizTalk Server 2020 auf Windows. Er liest BizTalk-Betriebsdaten ueber WMI aus `root\MicrosoftBizTalkServer`, erzeugt Checkmk-Local-Check-Ausgaben und kann dadurch ohne serverseitigen Python-Check in Checkmk 2.4 integriert werden. +`BizTalk Checkmk Pulse` ist ein robuster Checkmk-2.4-Local-Check fuer Microsoft +BizTalk Server 2020 auf Windows Server 2019. Die Anwendung trennt den +berechtigten BizTalk-Datenzugriff vollstaendig vom Checkmk-Agenten: -Der Ansatz ist fuer Umgebungen wie `ACC`, `DEV`, `TST` und `PRD` gedacht, in denen pro Umgebung ein BizTalk Server 2020 und ein SQL Server betrieben werden. SQL Server bleibt beim mitgelieferten Checkmk-MSSQL-Plugin; dieses Projekt ergaenzt die BizTalk-spezifische Sicht. +```text +Scheduled Task (dediziertes Dienstkonto oder gMSA) + | + | jede Minute: BizTalkCheckmkPulse.exe --collect + v +lokales BizTalk-WMI + BizTalk-SQL + Application Event Log + | + | atomarer, versionierter, SHA-256-geschuetzter Snapshot + v +%ProgramData%\BizTalkCheckmkPulse\data\biztalk-checkmk-pulse.snapshot + ^ + | nur lesen: BizTalkCheckmkPulse.exe --consume + | +Checkmk Windows Agent (LocalSystem) + | + v +sechs stabile Checkmk Local Checks +``` -Die Plattform- und SQL-Zielermittlung verwendet ausschliesslich die dokumentierte WMI-Klasse `MSBTS_GroupSetting`. Management-Datenbank und Master-MessageBox werden ueber `MgmtDbServerName`/`MgmtDbName` sowie `SubscriptionDBServerName`/`SubscriptionDBName` gelesen. Eine Klasse `MSBTS_MessageBoxSetting` gehoert nicht zum BizTalk-WMI-Schema und wird bewusst nicht abgefragt. +Damit bekommt `LocalSystem` keine BizTalk- oder SQL-Berechtigung. Nur das +dedizierte Provider-Konto wird in die konfigurierte BizTalk Server Read Only +Users-Gruppe aufgenommen. Der Checkmk-Consumer liest keine BizTalk-Datenbank, +verwendet kein WMI und nimmt keine Identitaetswechsel vor. -## Warum Local Check statt serverseitigem Check-Plugin? +## Warum die Architektur geaendert wurde -Checkmk 2.4 kann Windows-Agent-Plugins und Local Checks direkt ausfuehren. Fuer diese Umgebung ist ein C#/.NET-Framework-Programm mit `.cmd`-Wrapper die robusteste Variante: +Der ACC-Test vom 29.07.2026 zeigte: -- keine PowerShell-Ausfuehrung erforderlich -- keine BizTalk-ExplorerOM/OperationsOM-DLL als Build-Abhaengigkeit -- keine Python-Check-API-Abhaengigkeit auf der Checkmk-Site -- einfache Verteilung auf die BizTalk-Maschinen per Dateiablage, Softwareverteilung oder Agent Bakery -- automatische Service Discovery in Checkmk +```text +Login failed for user 'BEW\AV23AGPWBIO1$' +``` -## Erzeugte Checkmk-Services +Der Checkmk-Agent lief korrekt als `NT AUTHORITY\SYSTEM` und erreichte lokales +BizTalk-WMI. Datenbankgestuetzte WMI-Abfragen wurden jedoch am SQL Server als +Maschinenkonto `BEW\AV23AGPWBIO1$` abgewiesen. Eine Mitgliedschaft des +Computerkontos wuerde die BizTalk-Netzwerkberechtigung allen als `LocalSystem` +laufenden Diensten des Servers geben. -Standardmaessig entstehen diese stabilen Services: +Die neue Trennung reduziert diesen Berechtigungsumfang. Fuer BizTalk Server +2020 ist die konfigurierte `BizTalk Server Read Only Users`-Gruppe mit der +SQL-Datenbankrolle `BTS_READONLY_USERS` die bevorzugte Grenze. Die +`BizTalk Server Operators`-Gruppe ist nur ein dokumentierter Fallback, wenn +eine konkret benoetigte WMI-Klasse trotz bestaetigter Read-Only-Zuordnung +abgewiesen wird. Direkte SQL-Logins, manuelle Datenbankrollen und `sysadmin` +sind keine Loesung. + +## Robustheit + +Der Datenaustausch ist bewusst defensiv: + +- Der Provider schreibt zuerst eine eindeutige Temporaerdatei im Zielordner, + leert Betriebssystempuffer und ersetzt danach den Snapshot atomar. +- Der Snapshot enthaelt Formatversion, UTC-Zeit, Quellmaschine, + Provider-Identitaet, Zeilenanzahl und SHA-256 des Payloads. +- Der Consumer akzeptiert nur denselben Rechner, gueltiges UTF-8, intakte + Checkmk-Zeilen, korrekte SHA-256-Pruefsumme und ein maximales Alter von + standardmaessig 180 Sekunden. +- Fehlende, veraltete, abgeschnittene, manipulierte oder unlesbare Dateien + ergeben sechs gueltige `UNKNOWN`-Services statt einer kaputten Agent-Ausgabe. +- Ein exklusives Lock und die Task-Einstellung `IgnoreNew` verhindern + ueberlappende Providerlaeufe. +- Ein unerwarteter Providerfehler erzeugt nach Moeglichkeit einen aktuellen + `UNKNOWN`-Snapshot und einen ungleich null lautenden Task-Exitcode. +- Provider und Consumer protokollieren in taegliche Dateien; die Aufbewahrung + ist standardmaessig 30 Tage. +- Snapshotgroesse, WMI-/SQL-Timeouts, Log-Retention und Stale-Grenze sind + begrenzt und konfigurierbar. + +## Erzeugte Services + +Standardmaessig entstehen: - `BizTalk Platform` - `BizTalk SQL Access` @@ -27,323 +83,301 @@ Standardmaessig entstehen diese stabilen Services: - `BizTalk Runtime Artifacts` - `BizTalk Event Log` -Wenn `EnvironmentName=ACC`, `DEV`, `TST` oder `PRD` gesetzt wird, wird der Name vor den Suffix gesetzt, z.B. `BizTalk PRD Suspended Instances`. Das ist praktisch, wenn die Umgebung bereits im Hostnamen oder Ordner abgebildet ist, aber nicht zwingend noetig. +Mit `EnvironmentName=ACC`, `DEV`, `TST` oder `PRD` wird die Umgebung in den +Servicenamen aufgenommen, zum Beispiel `BizTalk ACC Platform`. -Details zu Statuslogik und Metriken stehen in [docs/CheckmkServices.md](docs/CheckmkServices.md). -Beispielausgaben stehen in [docs/ExampleOutput.md](docs/ExampleOutput.md). -Die konkrete ACC-Freigabeanleitung steht als Textdatei in [docs/ACC-WMI-SQL-Berechtigung.txt](docs/ACC-WMI-SQL-Berechtigung.txt). +Statuslogik und Metriken: [docs/CheckmkServices.md](docs/CheckmkServices.md) -## Build +Beispielausgaben: [docs/ExampleOutput.md](docs/ExampleOutput.md) -Voraussetzungen auf einem Windows-Build-Host: +## Voraussetzungen + +Build-Host: - Visual Studio 2019/2022 Build Tools oder Visual Studio - MSBuild im `PATH` - .NET Framework 4.7.2 Developer Pack -Build: +BizTalk-Server: + +- Windows Server 2019 +- BizTalk Server 2020 und lokaler Namespace + `root\MicrosoftBizTalkServer` +- .NET Framework 4.7.2 +- Checkmk Windows Agent +- administrativer Zugriff fuer die einmalige Installation +- dediziertes AD-Dienstkonto oder bevorzugt gMSA fuer den Provider + +Das Provider-Konto benoetigt: + +- lokales Recht zur Ausfuehrung als Scheduled Task +- lokalen Lese-/Ausfuehrungszugriff auf die installierte EXE +- Schreibzugriff nur auf Snapshot- und Logverzeichnis +- Mitgliedschaft in der exakt konfigurierten BizTalk Server Read Only + Users-Gruppe + +Es soll weder lokaler Administrator noch SQL-`sysadmin` sein. Fuer ein gMSA +muss der BizTalk-Server das verwaltete Kennwort abrufen duerfen und das Konto +lokal installiert sein. + +## Build und Tests ```cmd scripts\build-release.cmd -``` - -Build plus dependency-free regression tests: - -```cmd scripts\test-release.cmd -``` - -Deployment-Paket erstellen: - -```cmd scripts\package-release.cmd ``` -Ergebnis: +Das Paket wird unter `artifacts\BizTalkCheckmkPulse-deploy` erzeugt: ```text -artifacts\BizTalkCheckmkPulse-deploy\ +BizTalkCheckmkPulse-deploy\ + Install-BizTalkCheckmkPulse.ps1 + Uninstall-BizTalkCheckmkPulse.ps1 biztalk_checkmk_pulse.cmd - BizTalkCheckmkPulse\ + application\ BizTalkCheckmkPulse.exe BizTalkCheckmkPulse.exe.config ``` -Validierung nach dem Build: +Format-Self-Test ohne WMI, SQL oder Event Log: ```cmd -artifacts\BizTalkCheckmkPulse-deploy\biztalk_checkmk_pulse.cmd --self-test +artifacts\BizTalkCheckmkPulse-deploy\application\BizTalkCheckmkPulse.exe --self-test ``` -Der Self-Test fuehrt keine WMI-, SQL- oder Event-Log-Abfrage aus und muss genau sechs `OK`-Zeilen liefern. Der Test-Runner prueft zusaetzlich UNKNOWN-Fallbacks, dynamische Application-Services, FQDN-/Kurznamensvergleich ohne WQL-Namensfilter und den dokumentierten `MSBTS_GroupSetting`-Queryvertrag. Die zusaetzliche lokale Verifikation mit Mono/MSBuild 16 prueft Build und Ausgabeformat, ersetzt aber nicht den Windows-/BizTalk-Test. +Erwartet werden exakt sechs `OK`-Zeilen. Die Regressionstests pruefen +zusaetzlich Snapshot-Roundtrip, atomaren Ersatz, SHA-256-Manipulation, +Stale-Erkennung, stabile Fallbacks und die bestehenden BizTalk-WMI-Diagnosen. +Ein Mono-Build ist eine hilfreiche Quellcodepruefung, ersetzt aber nicht die +Windows-/BizTalk-Laufzeitvalidierung. -## Installation auf dem BizTalk-Server +## Berechtigung vorbereiten -Kopiere den Inhalt von `artifacts\BizTalkCheckmkPulse-deploy` nach: +Die exakte Read-Only-Gruppe wird in der BizTalk Administration Console unter +den Eigenschaften der BizTalk-Gruppe abgelesen. Ein bereits berechtigtes Konto +kann sie alternativ ermitteln: + +```powershell +Get-CimInstance ` + -Namespace root/MicrosoftBizTalkServer ` + -ClassName MSBTS_GroupSetting | + Select-Object Name, BizTalkReadOnlyUserGroup, BizTalkOperatorGroup, + MgmtDbServerName, MgmtDbName +``` + +Ein AD-Administrator nimmt das neue Provider-Konto in +`BizTalkReadOnlyUserGroup` auf. Bei einem gMSA endet der Kontoname mit `$`. +Nach AD-Replikation muss ein regulaeres Dienstkonto einen neuen Anmeldetoken +erhalten; bei gMSA wird der Task nach der Gruppenfreigabe neu gestartet. + +Die BizTalk-Konfiguration muss die Domain-Gruppe bereits als Windows-Login und +in `BizTalkMgmtDb`, `BizTalkMsgBoxDb`, `BizTalkDTADb`, +`BizTalkRuleEngineDb` sowie gegebenenfalls `BAMPrimaryImport` mit +`BTS_READONLY_USERS` abbilden. Eine fehlende Abbildung wird durch BizTalk- und +SQL-Administration fuer die Gruppe repariert, nicht als Einzelberechtigung +fuer das Provider-Konto. + +## Installation mit gMSA + +Deployment-Paket auf den BizTalk-Server kopieren. In administrativer Windows +PowerShell: + +```powershell +Set-Location C:\Temp\BizTalkCheckmkPulse-deploy + +# Optional, falls das gMSA noch nicht lokal installiert wurde: +Install-ADServiceAccount -Identity svc_biztalk_cmk +Test-ADServiceAccount -Identity svc_biztalk_cmk + +.\Install-BizTalkCheckmkPulse.ps1 ` + -CollectorAccount 'BEW\svc_biztalk_cmk$' ` + -Gmsa ` + -EnvironmentName ACC +``` + +## Installation mit regulaerem Dienstkonto + +```powershell +Set-Location C:\Temp\BizTalkCheckmkPulse-deploy +.\Install-BizTalkCheckmkPulse.ps1 ` + -CollectorAccount 'BEW\svc_biztalk_cmk' ` + -EnvironmentName ACC +``` + +Der Installer fragt das Kennwort ueber `Get-Credential` ab und speichert es +durch die Windows-Aufgabenplanung. Das Kennwort steht weder in der +Konfigurationsdatei noch in den Logs. + +Der Installer: + +1. kopiert EXE und Config nach + `%ProgramFiles%\BizTalkCheckmkPulse`, +2. erstellt `%ProgramData%\BizTalkCheckmkPulse\data` und `logs`, +3. setzt explizite ACLs fuer Administratoren, Provider und `LocalSystem`, +4. installiert nur den kleinen `.cmd`-Consumer unter + `%ProgramData%\checkmk\agent\local`, +5. registriert `BizTalk Checkmk Pulse Provider` minuetlich mit + `IgnoreNew`, fuenf Minuten Laufzeitlimit und zwei Wiederholungen, +6. fuehrt den Self-Test aus und startet den Provider einmalig. + +PowerShell wird nur fuer Installation und Betriebsdiagnose verwendet. Der +minuetliche Provider und der Checkmk-Consumer sind .NET-/CMD-Laufzeitcode und +haengen nicht von der PowerShell Execution Policy ab. + +## Verifikation auf dem Server + +Task und letzter Lauf: + +```powershell +Get-ScheduledTask -TaskName 'BizTalk Checkmk Pulse Provider' | + Select-Object TaskName, State +Get-ScheduledTaskInfo -TaskName 'BizTalk Checkmk Pulse Provider' | + Select-Object LastRunTime, LastTaskResult, NextRunTime +``` + +Provider-Log: + +```powershell +Get-ChildItem "$env:ProgramData\BizTalkCheckmkPulse\logs" | + Sort-Object LastWriteTime -Descending | + Select-Object -First 3 Name, Length, LastWriteTime + +Get-Content ` + "$env:ProgramData\BizTalkCheckmkPulse\logs\biztalk-checkmk-pulse-*.log" ` + -Tail 100 +``` + +Snapshot und Consumer: + +```powershell +Get-Item ` + "$env:ProgramData\BizTalkCheckmkPulse\data\biztalk-checkmk-pulse.snapshot" | + Select-Object FullName, Length, LastWriteTimeUtc + +& "$env:ProgramFiles\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe" --consume +``` + +Verbindlicher Test im echten `LocalSystem`-Kontext: + +```powershell +& "C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe" dump | + Select-String -Pattern "BizTalk|UNKNOWN|Snapshot|Permission" -Context 0,1 +``` + +Danach in Checkmk eine Service Discovery ausfuehren, die sechs Services +aufnehmen und Changes aktivieren. Ein zusaetzlicher Checkmk-Async-Cache ist +nicht erforderlich: Der Consumer liest nur eine kleine lokale Datei und der +Provider besitzt bereits seinen eigenen Minutentakt. + +## Dateisystem und Sicherheitsgrenzen ```text -%ProgramData%\checkmk\agent\local +%ProgramFiles%\BizTalkCheckmkPulse\ + BizTalkCheckmkPulse.exe + BizTalkCheckmkPulse.exe.config + +%ProgramData%\BizTalkCheckmkPulse\ + data\ + biztalk-checkmk-pulse.snapshot + biztalk-checkmk-pulse.snapshot.provider.lock + logs\ + biztalk-checkmk-pulse-YYYYMMDD.log + +%ProgramData%\checkmk\agent\local\ + biztalk_checkmk_pulse.cmd ``` -Zielstruktur: +ACL-Soll: -```text -%ProgramData%\checkmk\agent\local\biztalk_checkmk_pulse.cmd -%ProgramData%\checkmk\agent\local\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe -%ProgramData%\checkmk\agent\local\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe.config -``` +| Pfad | Provider | LocalSystem | Administratoren | +| --- | --- | --- | --- | +| Programm | Lesen/Ausfuehren | Lesen/Ausfuehren | Vollzugriff | +| `data` | Aendern | Lesen/Ausfuehren | Vollzugriff | +| `logs` | Aendern | Aendern | Vollzugriff | -Manueller Test auf dem BizTalk-Server: - -```cmd -"%ProgramData%\checkmk\agent\local\biztalk_checkmk_pulse.cmd" --self-test -"%ProgramData%\checkmk\agent\local\biztalk_checkmk_pulse.cmd" -``` - -Agent-Ausgabe wie Checkmk sie sieht: - -```cmd -"C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe" dump -``` - -Danach in Checkmk fuer den BizTalk-Host eine Service Discovery ausfuehren, die neuen Services aufnehmen und Changes aktivieren. +Der Snapshot enthaelt Monitoringzustand und kompakte Fehlerdetails, aber keine +Passwoerter oder Nachrichteninhalte. Der Consumer validiert die Datei trotzdem +vollstaendig, bevor er sie an Checkmk weitergibt. ## Konfiguration -Die Konfiguration liegt neben der EXE: +Datei: ```text -%ProgramData%\checkmk\agent\local\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe.config +%ProgramFiles%\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe.config ``` Wichtige Werte: | Key | Default | Bedeutung | | --- | --- | --- | -| `Server` | `.` | Lokaler BizTalk-Server. Remote-WMI ist moeglich, aber nicht empfohlen. | -| `ServicePrefix` | `BizTalk` | Prefix fuer alle Checkmk-Services. | -| `EnvironmentName` | leer | Optional `ACC`, `DEV`, `TST` oder `PRD`. | -| `QueryTimeoutSeconds` | `25` | WMI-Timeout pro Query. | -| `ProbeSqlConnectivity` | `true` | Prueft die integrierte Windows-Anmeldung an der per WMI ermittelten Management- und Master-MessageBox-Datenbank. | -| `SqlConnectionTimeoutSeconds` | `5` | Timeout je SQL-Ziel fuer Verbindungsaufbau und harmlose Testabfrage. | -| `WarnResumableThreshold` | `1` | WARN ab n resumable suspended instances. | -| `CritNonResumableThreshold` | `1` | CRIT ab n non-resumable suspended instances. | -| `AlertOnArtifactRuntimeIssues` | `false` | Wenn `true`, werden deaktivierte Receive Locations und inaktive Ports/Orchestrations als WARN gewertet. | -| `EmitPerApplicationSuspensionServices` | `false` | Erzeugt zusaetzliche Services pro Anwendung mit Suspensions. | -| `ProbeEventLog` | `true` | Liest das Windows Application Log nach BizTalk-bezogenen Sources. | -| `EventLogLookbackMinutes` | `60` | Zeitraum fuer Event-Log-Auswertung. | -| `EventLogWarnThreshold` | `1` | WARN ab n Errors oder Warnings. | -| `EventLogCritThreshold` | `10` | CRIT ab n Errors. | +| `EnvironmentName` | leer | Optional `ACC`, `DEV`, `TST`, `PRD`. | +| `SnapshotPath` | `%ProgramData%\BizTalkCheckmkPulse\data\...` | Gemeinsame Provider-/Consumer-Datei. | +| `SnapshotMaxAgeSeconds` | `180` | Ab diesem Alter liefert der Consumer `UNKNOWN`. | +| `SnapshotMaxBytes` | `1048576` | Harte Eingabegroesse fuer den Consumer. | +| `LogDirectory` | `%ProgramData%\BizTalkCheckmkPulse\logs` | Tageslogs. | +| `LogRetentionDays` | `30` | Provider bereinigt aeltere Logs. | +| `QueryTimeoutSeconds` | `25` | WMI-Timeout je Query. | +| `SqlConnectionTimeoutSeconds` | `5` | SQL-Timeout je Ziel. | +| `WarnResumableThreshold` | `1` | WARN ab n resumable Suspensions. | +| `CritNonResumableThreshold` | `1` | CRIT ab n non-resumable Suspensions. | +| `AlertOnArtifactRuntimeIssues` | `false` | WARN fuer bewusst inaktive Artefakte aktivieren. | +| `EmitPerApplicationSuspensionServices` | `false` | Zusaetzliche Anwendungsservices. | +| `EventLogLookbackMinutes` | `60` | Event-Log-Zeitfenster des Providers. | -Empfehlung: +Nach einer Config-Aenderung den Scheduled Task manuell starten. Der Consumer +liest den naechsten atomar publizierten Snapshot. -- In `DEV`, `TST`, `ACC`: `AlertOnArtifactRuntimeIssues=false`, damit bewusst gestoppte Artefakte nicht rauschen. -- In `PRD`: erst zwei Wochen beobachten; danach nur aktivieren, wenn die Runtime-Artefakte wirklich als Betriebsstandard immer aktiv sein muessen. -- `EmitPerApplicationSuspensionServices=true` nur verwenden, wenn Anwendungsteams eigene Services benoetigen. Sonst bleibt die Discovery schlanker. +## Fehlerbilder -## Integration in Checkmk Managed Services Edition 2.4 +| Beobachtung | Ursache / Massnahme | +| --- | --- | +| Alle sechs Services melden fehlenden Snapshot | Task, Provider-Log, Task-Konto und ACL pruefen. | +| Snapshot ist `stale` | `LastTaskResult`, Laufzeit, WMI-/SQL-Timeout und Log pruefen. | +| SHA-256 oder Format ungueltig | Datei nicht manuell bearbeiten; Datentraeger/AV und Schreibpfad pruefen, Task neu starten. | +| Provider meldet `Login failed` | Provider-Konto und exakt konfigurierte Read-Only-Gruppe sowie `BTS_READONLY_USERS` pruefen. | +| `Wmi/Schema` | Klasse/Properties gegen BizTalk-2020-Schema pruefen; keine Rechte ausweiten. | +| Nur Event Log `UNKNOWN` | lokalen Application-Log-Zugriff des Provider-Kontos pruefen. | +| Task-Result `2` | Parallelstart oder Snapshot-I/O; Log und Lock/ACL pruefen. | -Aufgaben der Checkmk-Kollegen: +Ausfuehrliche Betriebs- und Architekturdetails stehen in +[Dokumentation.md](Dokumentation.md). -1. BizTalk-Hosts in Checkmk anlegen oder bestehende Hosts pruefen. -2. Sicherstellen, dass der Checkmk Windows Agent installiert, registriert und erreichbar ist. -3. Plugin-Dateien auf die BizTalk-Server verteilen, manuell oder per Agent Bakery. -4. Optional `EnvironmentName` je Umgebung setzen, z.B. `ACC`, `DEV`, `TST` oder `PRD`. -5. Agent-Ausgabe mit `cmk-agent-ctl.exe dump` pruefen. -6. Service Discovery fuer jeden BizTalk-Host ausfuehren. -7. Gefundene `BizTalk ...` Services aufnehmen und Changes aktivieren. -8. Views, Dashboards, Servicegruppen und Benachrichtigungen fuer die BizTalk-Services konfigurieren. - -Manuelle Integration: - -1. Dateien auf dem BizTalk-Server nach `%ProgramData%\checkmk\agent\local` kopieren. -2. Optional `BizTalkCheckmkPulse.exe.config` je Umgebung anpassen. -3. Agent-Dump pruefen. -4. Service Discovery auf dem BizTalk-Host ausfuehren. -5. Services in ein BizTalk-Dashboard aufnehmen. - -Integration ueber Agent Bakery: - -1. Deployment-Dateien in der Checkmk-Site als Custom-Agent-Dateien bereitstellen. -2. Windows-Agent-Regel fuer die BizTalk-Hosts erstellen. -3. Agent backen und auf ACC/DEV/TST/PRD-BizTalk-Hosts ausrollen. -4. Discovery und Dashboard-Aufnahme durchfuehren. - -Die Checkmk-Dokumentation nennt fuer Windows Local Checks `%ProgramData%\checkmk\agent\local` und fuer Windows Agent Plugins `%ProgramData%\checkmk\agent\plugins`. Dieses Projekt nutzt bewusst `local`, weil der Zustand direkt vom Host berechnet und sofort als Checkmk-Service geliefert wird. - -## Berechtigungen fuer WMI und BizTalk-Datenbanken - -### Ausfuehrungskontext - -Der Checkmk Windows Agent und der Agent Controller laufen standardmaessig als `LocalSystem` (`NT AUTHORITY\SYSTEM`). Der `.cmd`-Wrapper und `BizTalkCheckmkPulse.exe` erben diesen Sicherheitskontext. - -Das Programm: - -- verbindet sich lokal mit `\\\root\MicrosoftBizTalkServer` -- setzt keine separaten WMI-Anmeldedaten -- verwendet kein Remote-WMI -- ruft keine veraendernden BizTalk-WMI-Methoden auf -- liest zusaetzlich nur das lokale Windows Application Event Log - -Fuer den lokalen WMI-Verbindungsaufbau und das lokale Application Event Log reichen die Rechte von `LocalSystem` normalerweise aus. Es muessen deshalb im Regelfall keine DCOM-, Firewall- oder WMI-Namespace-Freigaben eingerichtet werden. - -### Zugriff auf einen separaten SQL Server - -Einige BizTalk-WMI-Klassen beziehen ihre Daten aus der BizTalk Management- oder MessageBox-Datenbank. Wenn der SQL Server auf einer anderen Maschine laeuft, greift ein unter `LocalSystem` ausgefuehrter Prozess im Netzwerk mit dem Computerkonto des BizTalk-Servers zu: - -```text -DOMAIN\BIZTALKSERVER$ -``` - -Die weitreichenden lokalen Rechte von `LocalSystem` ergeben nicht automatisch Berechtigungen auf dem entfernten SQL Server. Deshalb kann die Verbindung zum lokalen WMI-Namespace funktionieren, waehrend einzelne SQL-gestuetzte BizTalk-WMI-Abfragen mit `Access denied`, `UnauthorizedAccessException` oder `UNKNOWN` fehlschlagen. - -### Einordnung des ACC-Befunds vom 29.07.2026 - -Der Agent-Dump von `AV23AGPWBIO1` zeigt den entscheidenden inneren Providerfehler: - -```text -COMException HRESULT=0x80131904 -Internal error from OLEDB provider: 'Login failed for user 'BEW\AV23AGPWBIO1$'.' -``` - -Damit ist der lokale Namespace `root\MicrosoftBizTalkServer` bereits erreichbar. Der BizTalk-WMI-Provider kann aber seine SQL-gestuetzten Abfragen nicht ausfuehren, weil SQL Server das Computerkonto `BEW\AV23AGPWBIO1$` ablehnt. Zusaetzliche DCOM-, Firewall- oder WMI-Namespace-Rechte sind fuer diesen konkreten Fehler nicht die richtige Massnahme. Die `UNKNOWN`-Zustaende bei Platform, Host Instances, Runtime Artifacts und Suspended Instances sowie `targets=0` bei SQL Access sind Folgewirkungen derselben fehlenden Berechtigung. - -`BizTalk Event Log` funktioniert unabhaengig von diesem SQL-Zugriff. Die dort sichtbaren zwei Fehler und sechs Warnungen sind echte Ereignisse im betrachteten Zeitfenster und nach Behebung der Berechtigung separat zu untersuchen. - -### Codekorrektur und Berechtigungsfreigabe sind zwei getrennte Schritte - -Die ueberarbeitete Version vergibt keine Windows-, Active-Directory- oder SQL-Rechte. Sie erkennt den eingebetteten SQL-Loginfehler nur korrekt, nennt das abgewiesene Konto und verhindert eine irrefuehrende Provider-/Konfigurationsdiagnose. Ohne Berechtigungsfreigabe bleibt der Check deshalb auch mit der neuen EXE `UNKNOWN`. - -Der Zugriff funktioniert erst, wenn `BEW\AV23AGPWBIO1$` Mitglied der exakt konfigurierten BizTalk-Operator-Gruppe ist und das erneuerte Maschinen-Token diese Mitgliedschaft enthaelt. Normalerweise ist diese Windows-Gruppe bereits durch die BizTalk-Konfiguration als SQL-Login beziehungsweise Datenbankbenutzer mit `BTS_OPERATORS` in den BizTalk-Datenbanken eingerichtet. Das Maschinenkonto erbt diese SQL-Rechte ueber seine Gruppenmitgliedschaft; ein eigener SQL-Login fuer `BEW\AV23AGPWBIO1$` ist dann weder erforderlich noch gewuenscht. - -### Test im echten Checkmk-Kontext - -Ein manueller Aufruf von `BizTalkCheckmkPulse.exe` oder des Wrappers verwendet das Konto der angemeldeten Person. Ein erfolgreicher manueller Test beweist daher nicht, dass die Ausfuehrung durch Checkmk als `LocalSystem` ebenfalls funktioniert. - -Der verbindliche Test erfolgt ueber den Agent Controller: +## Deinstallation ```powershell -& "C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe" dump | - Select-String -Pattern "BizTalk|Login failed|Access denied|Unauthorized|UNKNOWN" -Context 0,1 +.\Uninstall-BizTalkCheckmkPulse.ps1 ``` -Erwartet werden die sechs stabilen `BizTalk ...` Services mit plausiblen Daten. Insbesondere `BizTalk Platform` und `BizTalk SQL Access` duerfen nicht wegen eines WMI-, SQL- oder Berechtigungsfehlers `UNKNOWN` sein. +Das Skript fragt vor dem Entfernen. Mit `-KeepRuntimeData` bleiben Snapshot und +Logs erhalten: -Das Programm prueft Berechtigungen aktiv: - -- Der WMI-Namespace-Verbindungsaufbau und jede erforderliche WMI-Klasse werden getrennt bewertet. -- Fehler werden als `Permission`, `Connectivity`, `Timeout`, `Configuration`, `Schema` oder `Provider` klassifiziert. -- `BizTalk SQL Access` oeffnet mit integrierter Windows-Authentifizierung eine Verbindung zur ermittelten Management- und Master-MessageBox-Datenbank und fuehrt `SELECT 1` aus. -- Die Service-Ausgabe nennt Ausfuehrungsidentitaet, erwartete Netzwerkidentitaet, betroffene Komponente, technische Ursache und konkrete Massnahme. -- Ein im BizTalk-WMI-Provider eingebettetes `Login failed for user` wird als `Wmi/Permission` klassifiziert; das tatsaechlich abgewiesene Konto wird in die Diagnose und in `BizTalk SQL Access` uebernommen. -- Nach erfolgreicher Plattformabfrage zeigt `BizTalk Platform` mit `operator_group=` die von BizTalk konfigurierte Operator-Gruppe. -- Fehlgeschlagene Pflichtabfragen werden nie als leerer, erfolgreicher Datenbestand gewertet. Der betroffene Service wird `UNKNOWN`. - -`Sql/Configuration` weist je nach Detailtext entweder auf eine unvollstaendige WMI-Zielermittlung oder auf TLS-, Zertifikats-, SPN-/SSPI-Probleme hin. Die Diagnose empfiehlt bewusst nicht, SQL-Verschluesselung pauschal abzuschalten. - -`Wmi/Schema` kennzeichnet `InvalidClass` oder `InvalidQuery`. Das ist kein Berechtigungsfehler: Klassen- und Property-Namen muessen gegen das installierte BizTalk-WMI-Schema geprueft werden; eine Rechteerhoehung kann eine nicht vorhandene Klasse nicht erzeugen. Die technischen Details enthalten die ausgefuehrte WQL-Abfrage und deren Laufzeit bis zum Fehler. - -Der SQL-Zugriffstest ersetzt nicht das Checkmk-MSSQL-Plugin auf dem SQL-Server. Er prueft ausschliesslich, ob genau die Identitaet des BizTalk Local Checks die fuer seine Diagnose benoetigten BizTalk-Datenbankziele erreichen kann. - -Da bei fehlenden Rechten jeder Lauf einen abgewiesenen SQL-Login erzeugen kann, wird fuer den produktiven Betrieb eine asynchrone Ausfuehrung mit 300 Sekunden Cache empfohlen. Das begrenzt SQL-Logeintraege und Last, verzoegert einen Zustandswechsel aber um maximal fuenf Minuten. Beispiel in `%ProgramData%\checkmk\agent\check_mk.user.yml`: - -```yaml -local: - enabled: yes - execution: - - pattern: $CUSTOM_LOCAL_PATH$\biztalk_checkmk_pulse.cmd - async: yes - run: yes - cache_age: 300 +```powershell +.\Uninstall-BizTalkCheckmkPulse.ps1 -KeepRuntimeData ``` -### Vorgehen bei Berechtigungsfehlern +Die AD-Gruppenmitgliedschaft des Provider-Kontos wird bewusst nicht automatisch +geaendert und muss separat durch AD-/BizTalk-Administration entfernt werden. -1. Den Agent-Dienst und sein Startkonto kontrollieren: +## Certutil-Transport - ```powershell - Get-CimInstance Win32_Service | - Where-Object { $_.Name -match 'check|cmk' -or $_.DisplayName -match 'checkmk' } | - Select-Object Name, DisplayName, State, StartName - ``` +Zu jeder Uebergabe wird ein Source-ZIP und eine certutil-kompatible +Base64-Textdatei erzeugt. Auf Windows: -2. Die exakt konfigurierte Operator-Gruppe in der BizTalk Administration Console unter `BizTalk Group` > `Properties` > `General` > `BizTalk Operators Group` ablesen. Nicht blind vom Standardnamen ausgehen. Alternativ kann ein bereits berechtigtes Konto abfragen: +```cmd +certutil -decode biztalk-checkmk-pulse-source--.zip.b64.txt biztalk-checkmk-pulse-source.zip +certutil -hashfile biztalk-checkmk-pulse-source.zip SHA256 +tar -xf biztalk-checkmk-pulse-source.zip +``` - ```powershell - Get-CimInstance -Namespace root/MicrosoftBizTalkServer -ClassName MSBTS_GroupSetting | - Select-Object Name, BizTalkOperatorGroup, MgmtDbServerName, MgmtDbName - ``` - -3. Ein AD-Administrator nimmt das Computerobjekt in genau diese Gruppe auf. Fuer ACC ist das Computerobjekt `AV23AGPWBIO1`, dessen Netzwerkprincipal `BEW\AV23AGPWBIO1$` ist: - - ```powershell - Import-Module ActiveDirectory - $computer = Get-ADComputer -Identity 'AV23AGPWBIO1' - Add-ADGroupMember -Identity '' -Members $computer -WhatIf - ``` - - Nach Kontrolle der aufgeloesten Ziele wird derselbe Befehl ohne `-WhatIf` ausgefuehrt. Anschliessend die Mitgliedschaft mit `Get-ADPrincipalGroupMembership -Identity 'AV23AGPWBIO1'` pruefen. - -4. AD-Replikation abwarten. Die sicherste Aktivierung ist ein Neustart des BizTalk-Servers im Wartungsfenster. Ohne Neustart kann ein Administrator die Maschinen-Tickets mit `klist purge -li 0x3e7` verwerfen und danach den zuvor ermittelten Checkmk-Agent-Dienst neu starten. - -5. Den Test im echten Agent-Kontext wiederholen: - - ```powershell - & "C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe" dump | - Select-String -Pattern "BizTalk|Login failed|Wmi/Permission|Sql/Permission|UNKNOWN" -Context 0,1 - ``` - -6. Erwartet werden Platform und SQL Access mit vollstaendiger Zielermittlung, zwei erreichbaren Datenbankzielen und keine berechtigungsbedingten `UNKNOWN`-Services. Erst wenn eine konkret benannte Klasse danach weiter scheitert, wird deren Rollenanforderung mit BizTalk- und SQL-Administration untersucht. - -Die Operator-Rolle ist fuer grundlegende Administration und Monitoring vorgesehen und kann Zustands- und Message-Flow-Informationen lesen, ohne BizTalk-Konfiguration oder Nachrichteninhalte einzusehen. Das Plugin fuehrt ausschliesslich Leseabfragen aus. Direkte manuelle Aenderungen an BizTalk-SQL-Datenbankrollen sollten nicht vorgenommen werden; die von BizTalk konfigurierte Windows-Gruppe ist die vorgesehene Berechtigungsgrenze. - -Bleibt `Login failed` nach bestaetigter Gruppenmitgliedschaft, AD-Replikation und Token-Erneuerung bestehen, pruefen BizTalk- und SQL-Administration die Gruppenabbildung auf dem SQL Server. Die konfigurierte Domain-Gruppe muss als Windows-Gruppenlogin aufloesbar sein und ihre Datenbankbenutzer muessen den von BizTalk vorgesehenen Rollen angehoeren, insbesondere `BTS_OPERATORS` in `BizTalkMgmtDb` und `BizTalkMsgBoxDb`. Eine fehlende oder abweichende Abbildung wird mit der BizTalk-Konfiguration abgeglichen und fuer die Gruppe repariert, nicht durch einen ad-hoc Einzel-Login fuer das Maschinenkonto umgangen. - -Entscheidungsmatrix: - -| Ergebnis im Agent-Dump | Massnahme | -| --- | --- | -| Alle BizTalk-Services liefern plausible Daten | Keine Berechtigungsaenderung erforderlich. | -| Verbindung zu `root\MicrosoftBizTalkServer` scheitert | Namespace, BizTalk-WMI-Provider, WMI-Dienst und Namespace-ACL gezielt pruefen. Keine pauschalen WMI-Rechte vergeben. | -| Service meldet `Wmi/Schema` beziehungsweise `InvalidClass`/`InvalidQuery` | WMI-Klasse und Properties gegen das BizTalk-Schema pruefen. Keine Berechtigungen erweitern. | -| `BizTalk SQL Access` meldet `Sql/Permission` | Computerkonto `DOMAIN\BIZTALKSERVER$` in die konfigurierte BizTalk-Operator-Gruppe aufnehmen, Kerberos erneuern und erneut testen. Keine direkten BizTalk-DB-Rollen vergeben. | -| `BizTalk SQL Access` meldet `Sql/Connectivity` oder `Sql/Timeout` | Server-/Instanzname, DNS, SQL-Dienst, TCP-Protokoll, Port und Firewall aus Sicht des BizTalk-Servers pruefen. | -| SQL-gestuetzte BizTalk-Klassen liefern `0x80131904`, `Login failed for user` oder `UNKNOWN` | Das in der Meldung genannte Computerkonto in die exakt konfigurierte BizTalk-Operator-Gruppe aufnehmen. | -| `BizTalk Event Log` ist `UNKNOWN` | Zugriff auf das lokale Application Log pruefen; `LocalSystem` kann es normalerweise lesen. | -| Fehler bleibt trotz Operator-Rolle bestehen | Betroffene WMI-Klasse und BizTalk-/SQL-Rollenzuordnung mit den Fachadministratoren untersuchen. | - -### Sicherheitsabwaegung und Alternative - -Wird das Computerkonto in `BizTalk Server Operators` aufgenommen, erhalten alle Dienste, die auf diesem BizTalk-Server als `LocalSystem` laufen, diese Netzwerkberechtigung. Das ist vor dem Rollout mit der Security- und BizTalk-Administration abzustimmen. - -Falls diese Freigabe nicht zulaessig ist, ist die sauberere Alternative ein separater Collector unter einem dedizierten gMSA- oder Dienstkonto mit Operator-Rechten. Dieser Collector kann seine Checkmk-Ausgabe in eine Spool-Datei schreiben. Der Checkmk-Agent liest dann nur die bereits erzeugten Daten ein. Den gesamten Checkmk-Agent-Dienst sollte man nicht allein fuer dieses Plugin von `LocalSystem` auf ein anderes Konto umstellen, weil dadurch alle Agent-Sektionen und Local Checks betroffen sind. - -Empfohlene Alarmierung: - -- `BizTalk Platform`: `UNKNOWN` immer untersuchen, da dann WMI, Rechte oder Deployment betroffen sind. -- `BizTalk SQL Access`: `UNKNOWN` als Integrations-, Berechtigungs- oder Verbindungsproblem behandeln und die eingebettete Massnahme abarbeiten. -- `BizTalk Suspended Instances`: in `PRD` direkt alarmieren; in Nicht-PRD nach Betriebsbedarf. -- `BizTalk Host Instances`: `CRIT` alarmieren, weil gestoppte Host Instances Laufzeitverarbeitung verhindern koennen. -- `BizTalk Runtime Artifacts`: zunaechst beobachten; strengere Alarmierung erst aktivieren, wenn deaktivierte Artefakte nicht fachlich gewollt sind. -- `BizTalk Event Log`: Schwellwerte nach Beobachtungsphase anpassen. +Die konkrete Datei und SHA-256-Summe werden bei der Uebergabe genannt. ## Quellen -- Checkmk Local Checks: https://docs.checkmk.com/latest/en/localchecks.html -- Checkmk Windows Agent und Plugin-Pfade: https://docs.checkmk.com/latest/en/agent_windows.html -- Checkmk Agent-Based Plugin-Entwicklung: https://docs.checkmk.com/latest/en/devel_check_plugins.html -- Checkmk Bakery API: https://docs.checkmk.com/latest/en/bakery_api.html -- Checkmk MKP-Pakete: https://docs.checkmk.com/latest/en/mkps.html -- Microsoft BizTalk WMI `MSBTS_ServiceInstance`: https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-serviceinstance-wmi -- Microsoft BizTalk WMI `ServiceStatus`: https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-serviceinstance-servicestatus-property-wmi -- Microsoft BizTalk WMI `MSBTS_GroupSetting`: https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-groupsetting-wmi -- Microsoft BizTalk Windows-Gruppen und SQL-Rollen: https://learn.microsoft.com/en-us/biztalk/core/windows-groups-and-user-accounts-in-biztalk-server -- Microsoft BizTalk administrative Rollen: https://learn.microsoft.com/en-us/biztalk/core/access-control-for-administrative-roles -- Microsoft BizTalk Gruppen-Eigenschaften: https://learn.microsoft.com/en-us/biztalk/core/how-to-modify-group-properties -- Microsoft BizTalk WMI Core Server Classes: https://learn.microsoft.com/en-us/biztalk/core/technical-reference/core-server-classes -- Microsoft BizTalk Mindestberechtigungen: https://learn.microsoft.com/en-us/biztalk/core/minimum-security-user-rights -- Microsoft BizTalk Access Control: https://learn.microsoft.com/en-us/biztalk/core/access-control-and-data-security -- Microsoft LocalSystem und Computerkonten: https://learn.microsoft.com/en-us/entra/architecture/service-accounts-computer -- Microsoft `Add-ADGroupMember`: https://learn.microsoft.com/en-us/powershell/module/activedirectory/add-adgroupmember -- Microsoft `klist`: https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/klist -- Microsoft WMI Namespace Security: https://learn.microsoft.com/en-us/windows/win32/wmisdk/access-to-wmi-namespaces +- Microsoft: BizTalk `MSBTS_GroupSetting.BizTalkReadOnlyUserGroup` +- Microsoft: Windows Groups and User Accounts in BizTalk Server +- Microsoft: Managing BizTalk Server Security +- Checkmk: Windows Agent und Local Checks + +Die genauen Links stehen in [Dokumentation.md](Dokumentation.md). diff --git a/deployment/Install-BizTalkCheckmkPulse.ps1 b/deployment/Install-BizTalkCheckmkPulse.ps1 new file mode 100644 index 0000000..3adad98 --- /dev/null +++ b/deployment/Install-BizTalkCheckmkPulse.ps1 @@ -0,0 +1,171 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$CollectorAccount, + + [switch]$Gmsa, + + [ValidateSet("", "ACC", "DEV", "TST", "PRD")] + [string]$EnvironmentName = "", + + [string]$InstallDirectory = "$env:ProgramFiles\BizTalkCheckmkPulse", + + [string]$CheckmkLocalDirectory = "$env:ProgramData\checkmk\agent\local" +) + +$ErrorActionPreference = "Stop" +Set-StrictMode -Version 2.0 + +$taskName = "BizTalk Checkmk Pulse Provider" +$runtimeRoot = Join-Path $env:ProgramData "BizTalkCheckmkPulse" +$dataDirectory = Join-Path $runtimeRoot "data" +$logDirectory = Join-Path $runtimeRoot "logs" +$sourceApplication = Join-Path $PSScriptRoot "application" +$sourceWrapper = Join-Path $PSScriptRoot "biztalk_checkmk_pulse.cmd" +$targetExe = Join-Path $InstallDirectory "BizTalkCheckmkPulse.exe" +$targetConfig = "$targetExe.config" +$targetWrapper = Join-Path $CheckmkLocalDirectory "biztalk_checkmk_pulse.cmd" + +function Assert-Administrator { + $identity = [Security.Principal.WindowsIdentity]::GetCurrent() + $principal = New-Object Security.Principal.WindowsPrincipal($identity) + if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { + throw "Die Installation muss in einer administrativen Windows PowerShell ausgefuehrt werden." + } +} + +function Invoke-Icacls { + param( + [Parameter(Mandatory = $true)][string]$Path, + [Parameter(Mandatory = $true)][string[]]$Arguments + ) + + & icacls.exe $Path @Arguments | Out-Host + if ($LASTEXITCODE -ne 0) { + throw "icacls ist fuer '$Path' mit Exitcode $LASTEXITCODE fehlgeschlagen." + } +} + +Assert-Administrator + +if (-not (Test-Path -LiteralPath $sourceApplication -PathType Container)) { + throw "Quellverzeichnis fehlt: $sourceApplication" +} +if (-not (Test-Path -LiteralPath $sourceWrapper -PathType Leaf)) { + throw "Checkmk-Wrapper fehlt: $sourceWrapper" +} + +if ($Gmsa -and -not $CollectorAccount.EndsWith('$')) { + throw "Ein gMSA-Kontoname muss mit '$' enden, z.B. DOMAIN\svc_biztalk_cmk$." +} + +Write-Host "Installiere Programmdateien nach $InstallDirectory ..." +New-Item -ItemType Directory -Path $InstallDirectory -Force | Out-Null +Copy-Item -Path (Join-Path $sourceApplication "*") -Destination $InstallDirectory -Force + +[xml]$configuration = Get-Content -LiteralPath $targetConfig +$environmentSetting = $configuration.configuration.appSettings.add | + Where-Object { $_.key -eq "EnvironmentName" } | + Select-Object -First 1 +if ($null -eq $environmentSetting) { + throw "EnvironmentName fehlt in $targetConfig." +} +$environmentSetting.value = $EnvironmentName +$configuration.Save($targetConfig) + +Write-Host "Erzeuge Runtime-Verzeichnisse und Least-Privilege-ACLs ..." +New-Item -ItemType Directory -Path $runtimeRoot, $dataDirectory, $logDirectory -Force | Out-Null + +# Runtime-Root: Provider lesen, SYSTEM lesen, lokale Administratoren verwalten. +Invoke-Icacls -Path $runtimeRoot -Arguments @( + "/inheritance:r", + "/grant:r", + "*S-1-5-18:(OI)(CI)(RX)", + "*S-1-5-32-544:(OI)(CI)(F)", + "${CollectorAccount}:(OI)(CI)(RX)" +) + +# Daten: ausschliesslich Provider schreibt, LocalSystem liest den Snapshot. +Invoke-Icacls -Path $dataDirectory -Arguments @( + "/inheritance:r", + "/grant:r", + "*S-1-5-18:(OI)(CI)(RX)", + "*S-1-5-32-544:(OI)(CI)(F)", + "${CollectorAccount}:(OI)(CI)(M)" +) + +# Logs: Provider und LocalSystem-Consumer duerfen getrennte Diagnosezeilen anhaengen. +Invoke-Icacls -Path $logDirectory -Arguments @( + "/inheritance:r", + "/grant:r", + "*S-1-5-18:(OI)(CI)(M)", + "*S-1-5-32-544:(OI)(CI)(F)", + "${CollectorAccount}:(OI)(CI)(M)" +) + +Write-Host "Installiere Checkmk Local Check nach $CheckmkLocalDirectory ..." +New-Item -ItemType Directory -Path $CheckmkLocalDirectory -Force | Out-Null +Copy-Item -LiteralPath $sourceWrapper -Destination $targetWrapper -Force + +Write-Host "Registriere Scheduled Task '$taskName' ..." +$action = New-ScheduledTaskAction ` + -Execute $targetExe ` + -Argument "--collect" ` + -WorkingDirectory $InstallDirectory +$trigger = New-ScheduledTaskTrigger ` + -Once ` + -At (Get-Date).AddMinutes(1) ` + -RepetitionInterval (New-TimeSpan -Minutes 1) +$settings = New-ScheduledTaskSettingsSet ` + -MultipleInstances IgnoreNew ` + -ExecutionTimeLimit (New-TimeSpan -Minutes 5) ` + -StartWhenAvailable ` + -RestartCount 2 ` + -RestartInterval (New-TimeSpan -Minutes 1) + +if ($Gmsa) { + $principal = New-ScheduledTaskPrincipal ` + -UserId $CollectorAccount ` + -LogonType Password ` + -RunLevel Highest + $task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings -Principal $principal + Register-ScheduledTask -TaskName $taskName -InputObject $task -Force | Out-Null +} +else { + $credential = Get-Credential ` + -UserName $CollectorAccount ` + -Message "Kennwort fuer das dedizierte BizTalk-Monitoringkonto eingeben" + if ($credential.UserName -ne $CollectorAccount) { + throw "Das eingegebene Konto stimmt nicht mit CollectorAccount ueberein." + } + + $principal = New-ScheduledTaskPrincipal ` + -UserId $CollectorAccount ` + -LogonType Password ` + -RunLevel Highest + $task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings -Principal $principal + Register-ScheduledTask ` + -TaskName $taskName ` + -InputObject $task ` + -User $CollectorAccount ` + -Password $credential.GetNetworkCredential().Password ` + -Force | Out-Null +} + +Write-Host "Fuehre formatseitigen Self-Test aus ..." +$selfTest = & $targetExe --self-test +if ($LASTEXITCODE -ne 0 -or @($selfTest).Count -ne 6) { + throw "Self-Test fehlgeschlagen. Erwartet wurden sechs Checkmk-Zeilen." +} + +Write-Host "Starte den Provider einmalig ..." +Start-ScheduledTask -TaskName $taskName + +Write-Host "" +Write-Host "Installation abgeschlossen." +Write-Host "Naechste Pruefungen:" +Write-Host " Get-ScheduledTaskInfo -TaskName '$taskName'" +Write-Host " Get-Content '$logDirectory\biztalk-checkmk-pulse-*.log' -Tail 50" +Write-Host " & '$targetExe' --consume" +Write-Host " & 'C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe' dump" diff --git a/deployment/Uninstall-BizTalkCheckmkPulse.ps1 b/deployment/Uninstall-BizTalkCheckmkPulse.ps1 new file mode 100644 index 0000000..bbd89f6 --- /dev/null +++ b/deployment/Uninstall-BizTalkCheckmkPulse.ps1 @@ -0,0 +1,30 @@ +[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High")] +param( + [string]$InstallDirectory = "$env:ProgramFiles\BizTalkCheckmkPulse", + [string]$CheckmkLocalDirectory = "$env:ProgramData\checkmk\agent\local", + [switch]$KeepRuntimeData +) + +$ErrorActionPreference = "Stop" +$taskName = "BizTalk Checkmk Pulse Provider" +$runtimeRoot = Join-Path $env:ProgramData "BizTalkCheckmkPulse" +$wrapper = Join-Path $CheckmkLocalDirectory "biztalk_checkmk_pulse.cmd" + +if ($PSCmdlet.ShouldProcess($taskName, "Scheduled Task entfernen")) { + Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue +} +if (Test-Path -LiteralPath $wrapper) { + if ($PSCmdlet.ShouldProcess($wrapper, "Checkmk Local Check entfernen")) { + Remove-Item -LiteralPath $wrapper -Force + } +} +if (Test-Path -LiteralPath $InstallDirectory) { + if ($PSCmdlet.ShouldProcess($InstallDirectory, "Programmdateien entfernen")) { + Remove-Item -LiteralPath $InstallDirectory -Recurse -Force + } +} +if (-not $KeepRuntimeData -and (Test-Path -LiteralPath $runtimeRoot)) { + if ($PSCmdlet.ShouldProcess($runtimeRoot, "Snapshot und Logs entfernen")) { + Remove-Item -LiteralPath $runtimeRoot -Recurse -Force + } +} diff --git a/deployment/checkmk/biztalk_checkmk_pulse.cmd b/deployment/checkmk/biztalk_checkmk_pulse.cmd index 85ced31..100d4e9 100644 --- a/deployment/checkmk/biztalk_checkmk_pulse.cmd +++ b/deployment/checkmk/biztalk_checkmk_pulse.cmd @@ -1,11 +1,11 @@ @echo off setlocal -set "BASE=%~dp0BizTalkCheckmkPulse" +set "BASE=%ProgramFiles%\BizTalkCheckmkPulse" set "EXE=%BASE%\BizTalkCheckmkPulse.exe" if not exist "%EXE%" ( - echo 3 "BizTalk Platform" - BizTalkCheckmkPulse.exe fehlt unter %BASE%. Massnahme: Deployment-Paket und Dateirechte pruefen und danach cmk-agent-ctl.exe dump erneut ausfuehren. + echo 3 "BizTalk Platform" - BizTalkCheckmkPulse.exe fehlt unter %BASE%. Massnahme: Install-BizTalkCheckmkPulse.ps1 erneut als Administrator ausfuehren und danach cmk-agent-ctl.exe dump erneut ausfuehren. echo 3 "BizTalk SQL Access" - BizTalkCheckmkPulse.exe fehlt unter %BASE%. Massnahme: Deployment-Paket und Dateirechte pruefen. echo 3 "BizTalk Suspended Instances" - BizTalkCheckmkPulse.exe fehlt unter %BASE%. Massnahme: Deployment-Paket und Dateirechte pruefen. echo 3 "BizTalk Host Instances" - BizTalkCheckmkPulse.exe fehlt unter %BASE%. Massnahme: Deployment-Paket und Dateirechte pruefen. @@ -14,10 +14,10 @@ if not exist "%EXE%" ( exit /b 0 ) -"%EXE%" %* +"%EXE%" --consume %* set "EXITCODE=%ERRORLEVEL%" if not "%EXITCODE%"=="0" ( - echo 3 "BizTalk Platform" - BizTalkCheckmkPulse.exe konnte nicht erfolgreich gestartet werden, Exitcode=%EXITCODE%. Massnahme: .NET Framework 4.7.2, EXE/Config-Dateien und Windows Application Log pruefen. + echo 3 "BizTalk Platform" - Snapshot-Consumer konnte nicht erfolgreich gestartet werden, Exitcode=%EXITCODE%. Massnahme: .NET Framework 4.7.2, Installation und Provider-Log pruefen. echo 3 "BizTalk SQL Access" - BizTalkCheckmkPulse.exe endete mit Exitcode=%EXITCODE%. Massnahme: .NET Framework 4.7.2, EXE/Config-Dateien und Windows Application Log pruefen. echo 3 "BizTalk Suspended Instances" - BizTalkCheckmkPulse.exe endete mit Exitcode=%EXITCODE%. Massnahme: Programmstart und Windows Application Log pruefen. echo 3 "BizTalk Host Instances" - BizTalkCheckmkPulse.exe endete mit Exitcode=%EXITCODE%. Massnahme: Programmstart und Windows Application Log pruefen. diff --git a/docs/ACC-WMI-SQL-Berechtigung.txt b/docs/ACC-WMI-SQL-Berechtigung.txt index fe803c8..13500c8 100644 --- a/docs/ACC-WMI-SQL-Berechtigung.txt +++ b/docs/ACC-WMI-SQL-Berechtigung.txt @@ -1,152 +1,115 @@ -BizTalk Checkmk Pulse - ACC Berechtigungsfreigabe -Stand: 29.07.2026 +BizTalk Checkmk Pulse - ACC Berechtigung und Architektur +Stand: 30.07.2026 BEFUND ====== Host: AV23AGPWBIO1 -Lokale Identitaet: NT AUTHORITY\SYSTEM (LocalSystem) -Netzwerkidentitaet: BEW\AV23AGPWBIO1$ +Checkmk-Identitaet: NT AUTHORITY\SYSTEM +Alte Netzwerkidentitaet: BEW\AV23AGPWBIO1$ Fehler: COMException HRESULT=0x80131904 Innerer Fehler: Login failed for user 'BEW\AV23AGPWBIO1$' -Der lokale BizTalk-WMI-Namespace ist erreichbar. Der BizTalk-WMI-Provider wird -aber bei seinem SQL-Zugriff als Maschinenkonto abgewiesen. Deshalb sind -Platform, Host Instances, Runtime Artifacts und Suspended Instances UNKNOWN. -BizTalk SQL Access kann ohne MSBTS_GroupSetting keine Ziele ermitteln. +Der lokale BizTalk-WMI-Namespace war erreichbar. Datenbankgestuetzte +BizTalk-WMI-Abfragen scheiterten am SQL-Login des Maschinenkontos. -Keine DCOM-, Firewall- oder WMI-Namespace-Rechte erweitern. Keinen direkten -SQL-Login, keine manuellen BizTalk-Datenbankrollen und kein sysadmin fuer das -Maschinenkonto anlegen. +NEUE VERBINDLICHE ARCHITEKTUR +============================= -WICHTIG ZUR NEUEN VERSION -========================= +Das Maschinenkonto erhaelt keine BizTalk-Berechtigung. -Die neue EXE repariert keine Berechtigungen. Sie erkennt den eingebetteten -SQL-Loginfehler korrekt, zeigt BEW\AV23AGPWBIO1$ und klassifiziert die -Folgediagnose als Berechtigungsproblem. Ohne die nachfolgende Freigabe bleiben -die betroffenen Checks auch mit der neuen Version UNKNOWN. +1. Ein dediziertes Dienstkonto oder bevorzugt gMSA laeuft minuetlich als + Scheduled Task "BizTalk Checkmk Pulse Provider". +2. Dieses Konto fuehrt BizTalkCheckmkPulse.exe --collect aus. +3. Es schreibt atomar einen SHA-256-geschuetzten Snapshot nach: -Die alte Version kann nach korrekter Freigabe grundsaetzlich funktionieren. -Empfohlen ist die neue Version zusammen mit der Gruppenfreigabe, weil sie -Fehlerursache, Konto und Massnahme eindeutig anzeigt. + %ProgramData%\BizTalkCheckmkPulse\data\biztalk-checkmk-pulse.snapshot -FREIGABE -======== +4. Der Checkmk-Agent bleibt LocalSystem und startet nur den Consumer. +5. Der Consumer liest und validiert den Snapshot. Er fuehrt kein WMI und + keinen SQL-Zugriff aus. -1. Exakte Operator-Gruppe ermitteln +BERECHTIGUNG DES PROVIDER-KONTOS +================================ -In der BizTalk Administration Console: +Bevorzugte Rolle fuer BizTalk Server 2020: - BizTalk Group > Properties > General > BizTalk Operators Group + BizTalk Server Read Only Users + SQL-Datenbankrolle: BTS_READONLY_USERS -Nicht ungeprueft den Standardnamen verwenden. Mit einem bereits berechtigten -Konto ist alternativ folgende Abfrage moeglich: +Die exakt konfigurierte Gruppe mit einem bereits berechtigten Konto ermitteln: Get-CimInstance -Namespace root/MicrosoftBizTalkServer ` -ClassName MSBTS_GroupSetting | - Select-Object Name, BizTalkOperatorGroup, MgmtDbServerName, MgmtDbName + Select-Object Name, BizTalkReadOnlyUserGroup, BizTalkOperatorGroup, + MgmtDbServerName, MgmtDbName -2. Computerobjekt durch einen AD-Administrator aufnehmen +Das Provider-Konto durch AD-Administration in BizTalkReadOnlyUserGroup +aufnehmen. Nicht vom Standardnamen ausgehen. - Import-Module ActiveDirectory - $computer = Get-ADComputer -Identity 'AV23AGPWBIO1' - Add-ADGroupMember ` - -Identity '' ` - -Members $computer ` - -WhatIf +Die Operator-Gruppe ist nur eine begruendungspflichtige Eskalation, falls eine +konkret benoetigte WMI-Klasse trotz bestaetigter Read-Only-Zuordnung und neuem +Anmeldetoken verweigert wird. -Ziele kontrollieren und danach denselben Add-ADGroupMember-Befehl ohne --WhatIf ausfuehren. +NICHT DURCHFUEHREN +================== -Mitgliedschaft pruefen: +- keinen SQL-Einzellogin fuer Provider- oder Maschinenkonto +- keine ad-hoc BizTalk-Datenbankrollen +- kein db_owner oder sysadmin +- keine pauschalen DCOM-/WMI-ACL-Erweiterungen +- Checkmk-Agentdienst nicht auf das Provider-Konto umstellen +- Maschinenkonto BEW\AV23AGPWBIO1$ nicht fuer diese Loesung berechtigen - Get-ADPrincipalGroupMembership -Identity 'AV23AGPWBIO1' | - Select-Object Name, SamAccountName +INSTALLATION +============ -3. Neue Gruppenzugehoerigkeit aktivieren +gMSA: -AD-Replikation abwarten. Bevorzugt den BizTalk-Server in einem Wartungsfenster -neu starten. Alternativ in einer administrativen Shell: + .\Install-BizTalkCheckmkPulse.ps1 ` + -CollectorAccount 'BEW\svc_biztalk_cmk$' ` + -Gmsa ` + -EnvironmentName ACC - klist purge -li 0x3e7 +Regulaeres Dienstkonto: -Danach den zuvor eindeutig ermittelten Checkmk-Agent-Dienst neu starten. Falls -die neue Mitgliedschaft nicht wirksam wird, den Server neu starten. + .\Install-BizTalkCheckmkPulse.ps1 ` + -CollectorAccount 'BEW\svc_biztalk_cmk' ` + -EnvironmentName ACC -4. Im echten Checkmk-Kontext verifizieren +VERIFIKATION +============ + + Get-ScheduledTaskInfo -TaskName 'BizTalk Checkmk Pulse Provider' + + Get-Content ` + "$env:ProgramData\BizTalkCheckmkPulse\logs\*.log" ` + -Tail 100 + + Get-Item ` + "$env:ProgramData\BizTalkCheckmkPulse\data\biztalk-checkmk-pulse.snapshot" + + & "$env:ProgramFiles\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe" --consume & "C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe" dump | - Select-String ` - -Pattern "BizTalk|Login failed|Wmi/Permission|Sql/Permission|UNKNOWN" ` - -Context 0,1 + Select-String -Pattern "BizTalk|UNKNOWN|Permission|snapshot" -Context 0,1 -Erwartetes Ergebnis: +ERWARTET +======== -- BizTalk Platform ist OK und zeigt operator_group=. -- BizTalk SQL Access zeigt targets=2, available=2, - discovery_complete=True. -- Host Instances, Runtime Artifacts und Suspended Instances sind nicht mehr - wegen WMI-/SQL-Berechtigungen UNKNOWN. -- Kein Login failed fuer BEW\AV23AGPWBIO1$. - -Der separate Event-Log-Service zeigte im Screenshot zwei Fehler und sechs -Warnungen. Diese Ereignisse bleiben nach der Berechtigungsfreigabe fachlich zu -pruefen. - -WENN ES DANACH WEITERHIN SCHEITERT -================================== - -Normalerweise wurde die konfigurierte BizTalk-Operator-Gruppe bereits durch die -BizTalk-Konfiguration am SQL Server eingerichtet. Das Maschinenkonto erbt ueber -seine AD-Gruppenmitgliedschaft die Rolle BTS_OPERATORS und benoetigt keinen -eigenen SQL-Login. - -Bleibt "Login failed" trotz bestaetigter Mitgliedschaft, AD-Replikation und -Token-Erneuerung bestehen, prueft die SQL-Administration read-only: - - SELECT name, type_desc - FROM sys.server_principals - WHERE name = N'BEW\'; - -Danach jeweils in BizTalkMgmtDb und BizTalkMsgBoxDb: - - SELECT - member_principal.name AS member_name, - role_principal.name AS role_name - FROM sys.database_role_members AS drm - JOIN sys.database_principals AS role_principal - ON role_principal.principal_id = drm.role_principal_id - JOIN sys.database_principals AS member_principal - ON member_principal.principal_id = drm.member_principal_id - WHERE member_principal.name = - N'BEW\' - AND role_principal.name = N'BTS_OPERATORS'; - -Fehlt die Abbildung, gleichen BizTalk- und SQL-Administration sie mit der -BizTalk-Konfiguration ab und reparieren sie fuer die konfigurierte Gruppe. -Keinen Einzel-Login oder Sonderrollen fuer BEW\AV23AGPWBIO1$ als Umgehung -anlegen. - -SICHERHEIT -========== - -Die Gruppenmitgliedschaft des Computerobjekts steht allen Diensten zur -Verfuegung, die auf AV23AGPWBIO1 als LocalSystem laufen und auf -Netzwerkressourcen zugreifen. Mit Security und BizTalk-Betrieb abstimmen. - -Ist dies nicht akzeptabel, ist ein separater Collector unter einem dedizierten -gMSA-/Dienstkonto mit BizTalk-Operator-Rechten die sauberere Architektur. Nicht -allein fuer dieses Plugin die Identitaet des gesamten Checkmk-Agent-Dienstes -aendern. +- LastTaskResult = 0 +- Snapshot wird minuetlich aktualisiert +- Provider-Log nennt das dedizierte Konto +- BizTalk Platform zeigt read_only_group= +- BizTalk SQL Access zeigt targets=2 und available=2 +- keine berechtigungsbedingten UNKNOWN-Services +- LocalSystem besitzt auf data nur Lesezugriff QUELLEN ======= -https://learn.microsoft.com/en-us/windows/win32/services/localsystem-account -https://learn.microsoft.com/en-us/entra/architecture/service-accounts-computer +https://learn.microsoft.com/en-us/biztalk/core/managing-biztalk-server-security https://learn.microsoft.com/en-us/biztalk/core/windows-groups-and-user-accounts-in-biztalk-server -https://learn.microsoft.com/en-us/biztalk/core/access-control-for-administrative-roles -https://learn.microsoft.com/en-us/biztalk/core/how-to-modify-group-properties -https://learn.microsoft.com/en-us/powershell/module/activedirectory/add-adgroupmember -https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/klist +https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-groupsetting-biztalkreadonlyusergroup-property-wmi +https://docs.checkmk.com/latest/en/agent_windows.html +https://docs.checkmk.com/latest/en/localchecks.html diff --git a/docs/CheckmkServices.md b/docs/CheckmkServices.md index 61873fa..bcd68f5 100644 --- a/docs/CheckmkServices.md +++ b/docs/CheckmkServices.md @@ -8,12 +8,17 @@ Alle Services werden als Checkmk Local Checks ausgegeben. Das Format ist: Die Ausgabe ist so gebaut, dass sie auch mit Checkmk 2.4.0p4 kompatibel bleibt: zwischen den vier Feldern steht genau ein normales Leerzeichen. +Die fachlichen Daten werden minuetlich vom privilegierten Scheduled-Task- +Provider erzeugt. Der Checkmk-Agent liest als `LocalSystem` nur den +integritaets- und altersgeprueften Snapshot. Ist dieser Transport nicht +verfuegbar, liefern alle sechs stabilen Services `UNKNOWN`. + ## BizTalk Platform Zweck: - Prueft, ob der BizTalk-WMI-Namespace erreichbar ist. -- Zeigt Server, Gruppe, konfigurierte BizTalk-Operator-Gruppe, Management-DB und Master-MessageBox-DB aus `MSBTS_GroupSetting`. +- Zeigt Server, Gruppe, konfigurierte BizTalk-Read-Only-/Operator-Gruppe, Management-DB und Master-MessageBox-DB aus `MSBTS_GroupSetting`. Status: @@ -31,11 +36,12 @@ Metriken: Zweck: - Ermittelt Management- und Master-MessageBox-Datenbank ueber `MSBTS_GroupSetting`. -- Oeffnet mit der Windows-Identitaet des Checkmk-Agentprozesses eine integrierte SQL-Verbindung. +- Oeffnet mit der Windows-Identitaet des privilegierten Provider-Tasks eine integrierte SQL-Verbindung. - Fuehrt die harmlose Abfrage `SELECT 1` aus und schliesst die Verbindung sofort wieder. - Zeigt lokale Ausfuehrungsidentitaet und erwartete Netzwerkidentitaet an. -Der Service ersetzt nicht das Checkmk-MSSQL-Plugin. Er prueft den fuer das BizTalk-Monitoring relevanten Zugriff des Local Checks. +Der Service ersetzt nicht das Checkmk-MSSQL-Plugin. Er prueft den fuer das +BizTalk-Monitoring relevanten Zugriff des dedizierten Provider-Kontos. Status: diff --git a/docs/ExampleOutput.md b/docs/ExampleOutput.md index 1bfbd7f..fc7325d 100644 --- a/docs/ExampleOutput.md +++ b/docs/ExampleOutput.md @@ -14,8 +14,8 @@ ## Gesunder BizTalk-Server ```text -0 "BizTalk Platform" - BizTalk WMI and platform data reachable, server=BIZTALK01, group=BizTalkMgmtDb, operator_group=CONTOSO\BizTalk Server Operators, mgmt_db=SQL01\BizTalkMgmtDb, msgbox_db=SQL01\BizTalkMsgBoxDb -0 "BizTalk SQL Access" biztalk_sql_targets_total=2;;;0|biztalk_sql_targets_available=2;;;0|biztalk_sql_targets_failed=0;;1;0 SQL access targets=2, available=2, discovery_complete=True, execution_identity=NT AUTHORITY\SYSTEM, network_identity=\BIZTALK01$; databases=Management=SQL01\BizTalkMgmtDb:OK, MasterMessageBox=SQL01\BizTalkMsgBoxDb:OK +0 "BizTalk Platform" - BizTalk WMI and platform data reachable, server=BIZTALK01, group=BizTalkMgmtDb, read_only_group=CONTOSO\BizTalk Server Read Only Users, operator_group=CONTOSO\BizTalk Server Operators, mgmt_db=SQL01\BizTalkMgmtDb, msgbox_db=SQL01\BizTalkMsgBoxDb +0 "BizTalk SQL Access" biztalk_sql_targets_total=2;;;0|biztalk_sql_targets_available=2;;;0|biztalk_sql_targets_failed=0;;1;0 SQL access targets=2, available=2, discovery_complete=True, execution_identity=CONTOSO\svc_biztalk_cmk$, network_identity=CONTOSO\svc_biztalk_cmk$; databases=Management=SQL01\BizTalkMgmtDb:OK, MasterMessageBox=SQL01\BizTalkMsgBoxDb:OK 0 "BizTalk Suspended Instances" biztalk_suspended_total=0;;;0|biztalk_suspended_resumable=0;1;;0|biztalk_suspended_nonresumable=0;;1;0 No suspended BizTalk service instances found. 0 "BizTalk Host Instances" biztalk_host_instances_total=4;;;0|biztalk_host_instances_started=4;;;0|biztalk_host_instances_stopped=0;;1;0|biztalk_host_instances_pending=0;1;;0|biztalk_host_instances_unknown=0;;1;0 Host instances total=4, started=4, stopped=0, pending=0, unknown=0 0 "BizTalk Runtime Artifacts" biztalk_applications=8;;;0|biztalk_receive_locations=24;;;0|biztalk_receive_locations_disabled=0;1;;0|biztalk_send_ports=31;;;0|biztalk_send_ports_started=31;;;0|biztalk_send_ports_inactive=0;1;;0|biztalk_send_ports_unknown=0;;1;0|biztalk_orchestrations=12;;;0|biztalk_orchestrations_started=12;;;0|biztalk_orchestrations_inactive=0;1;;0|biztalk_orchestrations_unknown=0;;1;0 Applications=8; receive_locations total=24, disabled=0; send_ports total=31, started=31, inactive=0, unknown=0; orchestrations total=12, started=12, inactive=0, unknown=0 @@ -31,20 +31,20 @@ ## SQL-Berechtigungsfehler ```text -3 "BizTalk SQL Access" biztalk_sql_targets_total=2;;;0|biztalk_sql_targets_available=0;;;0|biztalk_sql_targets_failed=2;;1;0 SQL access targets=2, available=0, discovery_complete=True, execution_identity=NT AUTHORITY\SYSTEM, network_identity=\BIZTALK01$; databases=Management=SQL01\BizTalkMgmtDb:FAILED, MasterMessageBox=SQL01\BizTalkMsgBoxDb:FAILED; diagnostics: Sql/Permission [Management SQL01\BizTalkMgmtDb]: SQL Server hat die integrierte Anmeldung abgelehnt. Erwartete Netzwerkidentitaet: \BIZTALK01$. Massnahme: \BIZTALK01$ in die konfigurierte BizTalk-Operator-Gruppe aufnehmen; keine direkten BizTalk-DB-Rollen vergeben. Danach Kerberos-Tickets erneuern und den Agent-Dump wiederholen. Technik: SqlException Number=18456: Login failed for user +3 "BizTalk SQL Access" biztalk_sql_targets_total=2;;;0|biztalk_sql_targets_available=0;;;0|biztalk_sql_targets_failed=2;;1;0 SQL access targets=2, available=0, discovery_complete=True, execution_identity=CONTOSO\svc_biztalk_cmk$, network_identity=CONTOSO\svc_biztalk_cmk$; databases=Management=SQL01\BizTalkMgmtDb:FAILED, MasterMessageBox=SQL01\BizTalkMsgBoxDb:FAILED; diagnostics: Sql/Permission [Management SQL01\BizTalkMgmtDb]: SQL Server hat die integrierte Anmeldung abgelehnt. Massnahme: Provider-Konto und konfigurierte BizTalk-Read-Only-Gruppe mit BTS_READONLY_USERS pruefen; keine direkten BizTalk-DB-Rollen vergeben. Technik: SqlException Number=18456: Login failed for user ``` ## WMI-Berechtigungsfehler ```text -3 "BizTalk Suspended Instances" - Suspendierte Instanzen konnten nicht verlaesslich gelesen werden. Ein leerer Datenbestand wird nicht als OK gewertet.; diagnostics: Wmi/Permission [MSBTS_ServiceInstance]: Zugriff auf MSBTS_ServiceInstance wurde im LocalSystem-Kontext verweigert. Massnahme: Agent-Dump pruefen und Computerkonto \BIZTALK01$ zunaechst der konfigurierten BizTalk-Operator-Gruppe zuordnen. Technik: UnauthorizedAccessException HRESULT=0x80070005: Access is denied +3 "BizTalk Suspended Instances" - Suspendierte Instanzen konnten nicht verlaesslich gelesen werden. Ein leerer Datenbestand wird nicht als OK gewertet.; diagnostics: Wmi/Permission [MSBTS_ServiceInstance]: Zugriff des Provider-Kontos auf MSBTS_ServiceInstance wurde verweigert. Massnahme: Provider-Identitaet und konfigurierte BizTalk-Read-Only-Gruppe pruefen. Technik: UnauthorizedAccessException HRESULT=0x80070005: Access is denied ``` -## SQL-Loginfehler innerhalb des BizTalk-WMI-Providers +## Fehlender oder veralteter Provider-Snapshot ```text -3 "BizTalk Platform" - BizTalk-Plattformdaten konnten nicht vollstaendig gelesen werden.; diagnostics: Wmi/Permission [MSBTS_GroupSetting]: BizTalk-WMI-Provider konnte den SQL-Zugriff fuer BEW\AV23AGPWBIO1$ nicht anmelden. Massnahme: Computerkonto BEW\AV23AGPWBIO1$ der in der BizTalk-Gruppe konfigurierten BizTalk-Operator-Gruppe zuordnen; keine direkten SQL-Logins oder Datenbankrollen vergeben. Technik: COMException HRESULT=0x80131904: Internal error from OLEDB provider: 'Login failed for user 'BEW\AV23AGPWBIO1$'.' -3 "BizTalk SQL Access" biztalk_sql_targets_total=0;;;0|biztalk_sql_targets_available=0;;;0|biztalk_sql_targets_failed=0;;1;0 SQL access targets=0, available=0, discovery_complete=False, execution_identity=NT AUTHORITY\SYSTEM, network_identity=BEW\AV23AGPWBIO1$; diagnostics: Sql/Permission [BizTalk database discovery]: SQL-Zielermittlung ist unvollstaendig, weil der BizTalk-WMI-Provider beim SQL-Zugriff abgewiesen wurde. +3 "BizTalk Platform" - Privilegierter BizTalk-Datensnapshot nicht verfuegbar: Snapshot is stale: age=241s, maximum=180s. Massnahme: Scheduled Task 'BizTalk Checkmk Pulse Provider', Provider-Log, Snapshot-Alter und ACL pruefen. +3 "BizTalk SQL Access" - Privilegierter BizTalk-Datensnapshot nicht verfuegbar: Snapshot is stale: age=241s, maximum=180s. Massnahme: Scheduled Task 'BizTalk Checkmk Pulse Provider', Provider-Log, Snapshot-Alter und ACL pruefen. ``` ## WMI-Schemafehler @@ -56,5 +56,5 @@ ## Fehlendes Deployment ```text -3 "BizTalk Platform" - BizTalkCheckmkPulse.exe fehlt unter C:\ProgramData\checkmk\agent\local\BizTalkCheckmkPulse. Massnahme: Deployment-Paket und Dateirechte pruefen und danach cmk-agent-ctl.exe dump erneut ausfuehren. +3 "BizTalk Platform" - BizTalkCheckmkPulse.exe fehlt unter C:\Program Files\BizTalkCheckmkPulse. Massnahme: Install-BizTalkCheckmkPulse.ps1 erneut als Administrator ausfuehren und danach cmk-agent-ctl.exe dump erneut ausfuehren. ``` diff --git a/docs/Integration.md b/docs/Integration.md index 5ba7251..1299fad 100644 --- a/docs/Integration.md +++ b/docs/Integration.md @@ -1,247 +1,142 @@ -# Integration in Checkmk +# Checkmk-Integration und Betriebsuebergabe -## Zielstruktur auf dem BizTalk-Server +## Verantwortungsgrenzen + +| Team | Aufgabe | +| --- | --- | +| AD/Security | dediziertes Dienstkonto oder gMSA; Aufnahme in exakt konfigurierte BizTalk-Read-Only-Gruppe | +| BizTalk/SQL | Gruppenabbildung und `BTS_READONLY_USERS` bestaetigen; keine Einzelrechte | +| Windows | Paket installieren, ACL und Scheduled Task pruefen | +| Checkmk | Wrapper verteilen beziehungsweise Installation koordinieren, Discovery und Alarmierung | + +Der Checkmk-Agent bleibt `LocalSystem`. Er greift weder auf BizTalk-WMI noch +auf SQL zu. Der minuetliche Scheduled Task sammelt unter dem privilegierten +Provider-Konto und publiziert einen validierbaren Snapshot. + +## Installation + +gMSA: + +```powershell +.\Install-BizTalkCheckmkPulse.ps1 ` + -CollectorAccount 'BEW\svc_biztalk_cmk$' ` + -Gmsa ` + -EnvironmentName ACC +``` + +Regulaeres Dienstkonto: + +```powershell +.\Install-BizTalkCheckmkPulse.ps1 ` + -CollectorAccount 'BEW\svc_biztalk_cmk' ` + -EnvironmentName ACC +``` + +Der Installer legt den Local Check hier ab: ```text %ProgramData%\checkmk\agent\local\biztalk_checkmk_pulse.cmd -%ProgramData%\checkmk\agent\local\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe -%ProgramData%\checkmk\agent\local\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe.config ``` -## Manuelle Installation - -1. Release-Paket auf den BizTalk-Server kopieren. -2. Inhalt nach `%ProgramData%\checkmk\agent\local` kopieren. -3. Optional `EnvironmentName` in der `.exe.config` setzen. -4. Test ausfuehren: - -```cmd -"%ProgramData%\checkmk\agent\local\biztalk_checkmk_pulse.cmd" --self-test -"%ProgramData%\checkmk\agent\local\biztalk_checkmk_pulse.cmd" -``` - -5. Agent-Ausgabe pruefen: - -```cmd -"C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe" dump -``` - -6. In Checkmk: - -- Host oeffnen -- Service Discovery ausfuehren -- gefundene `BizTalk ...` Services aufnehmen -- Changes aktivieren - -## Aufgaben in Checkmk - -Die Checkmk-Kollegen muessen kein serverseitiges Python-Check-Plugin installieren. Das Plugin liefert bereits fertige Local-Check-Services ueber den Windows Agent. In Checkmk selbst sind diese Schritte erforderlich: - -1. BizTalk-Hosts fuer `ACC`, `DEV`, `TST` und `PRD` anlegen oder vorhandene Hosts pruefen. -2. Windows-Agent-Status pruefen: Host muss Agent-Daten liefern. -3. Nach Installation des Local Checks den Agent-Dump pruefen. -4. Service Discovery fuer jeden BizTalk-Host ausfuehren. -5. Gefundene `BizTalk ...` Services aufnehmen. -6. Changes aktivieren. -7. Views oder Dashboards mit Filter `Service starts with: BizTalk` anlegen. -8. Benachrichtigungen und Eskalationen je Umgebung definieren. - -Empfohlene Service-Behandlung: - -| Service | Empfehlung | -| --- | --- | -| `BizTalk Platform` | `UNKNOWN` immer als Integrationsproblem behandeln. | -| `BizTalk SQL Access` | `UNKNOWN` anhand der Kategorie `Permission`, `Connectivity`, `Timeout`, `Configuration` oder `Provider` bearbeiten. | -| `BizTalk Suspended Instances` | In `PRD` alarmieren; in `ACC`/`TST`/`DEV` nach Teamvereinbarung. | -| `BizTalk Host Instances` | `CRIT` alarmieren. | -| `BizTalk Runtime Artifacts` | Erst beobachten; strenge Alarmierung nur bei klar definiertem Runtime-Sollzustand. | -| `BizTalk Event Log` | Schwellwerte nach Beobachtungsphase feinjustieren. | - -## Agent Bakery - -Checkmk Managed Services Edition 2.4 enthaelt die kommerziellen Mechanismen fuer Agent Bakery. Fuer einen sauberen Rollout: - -1. Deployment-Dateien als Custom Files oder ueber ein spaeteres MKP bereitstellen. -2. Regel nur auf BizTalk-Hosts anwenden, z.B. Host-Tag `application:biztalk`. -3. Gebackenen Windows-Agenten fuer die BizTalk-Hosts installieren. -4. Service Discovery ausfuehren. - -Hinweis: Dieses Repository enthaelt bewusst noch kein Bakery-Python-Plugin. Die manuelle bzw. dateibasierte Verteilung ist die risikoarme erste Version. Eine Bakery-Erweiterung ist eine sinnvolle Version-2-Ausbaustufe. - -## Berechtigungen fuer WMI und BizTalk-Datenbanken - -### Ausfuehrung als LocalSystem - -Der Checkmk Windows Agent und der Agent Controller laufen standardmaessig unter `LocalSystem` (`NT AUTHORITY\SYSTEM`). Dadurch werden auch `biztalk_checkmk_pulse.cmd` und `BizTalkCheckmkPulse.exe` in diesem Kontext gestartet. - -Das Plugin verbindet sich mit dem lokalen Namespace `\\\root\MicrosoftBizTalkServer`, setzt keine separaten Anmeldedaten und verwendet kein Remote-WMI. Es fuehrt ausschliesslich WMI-Leseabfragen aus und liest das lokale Windows Application Event Log. Fuer diese lokalen Zugriffe sind normalerweise keine zusaetzlichen DCOM-, Firewall- oder WMI-Namespace-Freigaben erforderlich. - -### Besonderheit bei getrenntem SQL Server - -Mehrere BizTalk-WMI-Klassen lesen Daten aus der BizTalk Management- oder MessageBox-Datenbank. Bei einem getrennten SQL Server verwendet `LocalSystem` fuer diesen Netzwerkzugriff das Active-Directory-Computerkonto des BizTalk-Servers: +Die EXE liegt zentral hier: ```text -DOMAIN\BIZTALKSERVER$ +%ProgramFiles%\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe ``` -Lokale Administratorrechte von `LocalSystem` gelten nicht automatisch auf dem entfernten SQL Server. Daher koennen SQL-gestuetzte WMI-Abfragen fehlschlagen, obwohl der lokale WMI-Namespace grundsaetzlich erreichbar ist. +## Checkmk-Agent-Konfiguration -### Konkreter ACC-Befund +Der Wrapper darf synchron laufen, da er nur einen kleinen lokalen Snapshot +liest. Eine alte Async-Regel mit `cache_age: 300` aus der direkten +WMI-Architektur soll entfernt werden; andernfalls addiert sie eine unnoetige +Verzoegerung zum minuetlichen Provider. -Auf `AV23AGPWBIO1` meldet der Provider `COMException HRESULT=0x80131904` und als inneren SQL-Fehler `Login failed for user 'BEW\AV23AGPWBIO1$'`. Das beweist zugleich: +Aktive Agentkonfiguration pruefen: -- der Check laeuft als `NT AUTHORITY\SYSTEM` -- der lokale BizTalk-WMI-Namespace ist erreichbar -- der Netzwerkzugriff erfolgt als Maschinenkonto `BEW\AV23AGPWBIO1$` -- dieses Konto besitzt noch nicht die durch BizTalk vermittelte SQL-Berechtigung +```powershell +& "C:\Program Files (x86)\checkmk\service\check_mk_agent.exe" showconfig local +``` -Die vielen `UNKNOWN`-Services sind Folgewirkungen desselben Fehlers. WMI-Namespace-ACL, DCOM und Firewall muessen fuer diesen Befund nicht erweitert werden. Die zwei Fehler und sechs Warnungen in `BizTalk Event Log` sind davon unabhaengig und separat auszuwerten. - -### Was die neue Version aendert - und was nicht - -Die neue Version verbessert Diagnose und Betriebsfuehrung: - -- `0x80131904` mit `Login failed for user` wird als `Wmi/Permission` erkannt. -- Das wirklich abgewiesene Konto wird aus der Providerfehlermeldung uebernommen. -- Die unvollstaendige SQL-Zielermittlung wird als `Sql/Permission` statt als allgemeine Konfigurationstoerung ausgegeben. -- Nach erfolgreichem Zugriff wird die konfigurierte Gruppe als `operator_group=` sichtbar. - -Die EXE kann und darf jedoch keine AD-Gruppenmitgliedschaft, SQL-Logins oder Datenbankrollen anlegen. Die alte Version kann nach korrekter Berechtigungsfreigabe grundsaetzlich arbeiten; die neue Version ohne Freigabe bleibt weiterhin `UNKNOWN`. Der Sollzustand ist daher: neue Version plus korrekte Gruppenberechtigung. - -### Verbindlicher Funktionstest - -Der direkte Start des Wrappers in einer administrativen Eingabeaufforderung ist nur ein Vorabtest, weil er mit dem angemeldeten Benutzer laeuft. Fuer die Berechtigungspruefung ist die Ausgabe unter dem echten Checkmk-Kontext massgeblich: +Verbindlicher Agent-Dump: ```powershell & "C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe" dump | - Select-String -Pattern "BizTalk|Login failed|Access denied|Unauthorized|UNKNOWN" -Context 0,1 + Select-String -Pattern "BizTalk|UNKNOWN|snapshot|Permission" -Context 0,1 ``` -Die Ausgabe muss die sechs stabilen `BizTalk ...` Services mit plausiblen Daten enthalten. `Access denied`, `UnauthorizedAccessException`, `Sql/Permission` und ein berechtigungsbedingtes `UNKNOWN` weisen auf eine fehlende Rollenzuordnung hin. +## Service Discovery -Die EXE klassifiziert WMI- und SQL-Probleme und schreibt zu jeder Diagnose `Massnahme:` und `Technik:`. WMI-Schemafehler (`InvalidClass`/`InvalidQuery`) werden separat als `Wmi/Schema` ausgewiesen und duerfen nicht durch Rechteerweiterungen behandelt werden. Eine fehlgeschlagene erforderliche WMI-Abfrage erzeugt beim betroffenen Service immer `UNKNOWN`; fehlende Daten werden nicht als Nullbestand und damit nicht als `OK` ausgegeben. +1. Provider dreimal erfolgreich laufen lassen. +2. `LastTaskResult=0`, frischen Snapshot und Log pruefen. +3. Agent-Dump im `LocalSystem`-Kontext pruefen. +4. Service Discovery fuer den BizTalk-Host ausfuehren. +5. sechs stabile Services aufnehmen. +6. Changes aktivieren. +7. Views, Servicegruppen und Benachrichtigungen einrichten. -Fuer den produktiven Betrieb wird empfohlen, den Local Check alle 300 Sekunden asynchron auszufuehren. Dadurch erzeugen fehlende SQL-Rechte nicht bei jedem Agent-Abruf neue fehlgeschlagene Logins. In `%ProgramData%\checkmk\agent\check_mk.user.yml`: +Host-Tags: -```yaml -local: - enabled: yes - execution: - - pattern: $CUSTOM_LOCAL_PATH$\biztalk_checkmk_pulse.cmd - async: yes - run: yes - cache_age: 300 +```text +env:ACC|DEV|TST|PRD +app:biztalk ``` -Alternativ koennen die Checkmk-Kollegen in der Agent Bakery die Regeln `Set execution mode for plug-ins and local checks` und `Set cache age for plug-ins and local checks` verwenden. Der Cache reduziert Last und SQL-Fehlerlogs; ein Zustandswechsel wird dadurch um maximal die konfigurierte Cache-Zeit verzoegert. - -### Least-Privilege-Vorgehen - -1. Dienstkonto mit `Get-CimInstance Win32_Service` kontrollieren; der ACC-Dump bestaetigt bereits `NT AUTHORITY\SYSTEM`. -2. In der BizTalk Administration Console unter `BizTalk Group` > `Properties` > `General` den Wert `BizTalk Operators Group` ablesen. Alternativ kann ein berechtigtes Konto `MSBTS_GroupSetting.BizTalkOperatorGroup` abfragen. -3. Ein AD-Administrator fuegt das Computerobjekt `AV23AGPWBIO1` genau dieser Gruppe hinzu: - - ```powershell - Import-Module ActiveDirectory - $computer = Get-ADComputer -Identity 'AV23AGPWBIO1' - Add-ADGroupMember -Identity '' -Members $computer -WhatIf - ``` - - Erst nach Kontrolle ohne `-WhatIf` ausfuehren. Keine direkten SQL-Logins, Datenbankrollen oder `sysadmin`-Rechte fuer `BEW\AV23AGPWBIO1$` anlegen. - -4. AD-Replikation abwarten und den Server im Wartungsfenster neu starten. Alternativ Maschinen-Tickets in einer administrativen Shell mit `klist purge -li 0x3e7` verwerfen und den Checkmk-Agent-Dienst neu starten. -5. Agent-Dump erneut ausfuehren. Erwartet werden `operator_group=<...>`, `targets=2`, `available=2`, `discovery_complete=True` und keine berechtigungsbedingten `UNKNOWN`-Services. -6. Nur bei weiterhin abgelehnten, konkret identifizierten WMI-Klassen mit BizTalk- und SQL-Administration klaeren, ob erweiterte Rechte erforderlich sind. - -Die BizTalk-Operator-Rolle ist fuer grundlegende Administration und Monitoring vorgesehen. Sie kann Zustands- und Message-Flow-Informationen lesen, darf aber keine Konfiguration oder Nachrichteninhalte einsehen. Das entspricht dem lesenden Funktionsumfang dieses Plugins. Keine direkten SQL-Rollen in den BizTalk-Datenbanken hinzufuegen; die von BizTalk konfigurierte Windows-Gruppe soll die Berechtigungen vermitteln. - -### Wenn die Gruppenmitgliedschaft allein nicht genuegt - -Normalerweise hat die BizTalk-Konfiguration die konfigurierte Domain-Gruppe bereits auf dem SQL Server abgebildet und ihren Datenbankbenutzern die Rolle `BTS_OPERATORS` zugeordnet. Das Maschinenkonto benoetigt dann keinen eigenen SQL-Login, sondern erbt den Zugriff ueber die AD-Gruppe. - -Bleibt der Loginfehler nach AD-Replikation und Token-Erneuerung bestehen, prueft die SQL-Administration zunaechst read-only den Gruppenlogin: - -```sql -SELECT name, type_desc -FROM sys.server_principals -WHERE name = N'BEW\'; -``` - -Anschliessend wird in `BizTalkMgmtDb` und `BizTalkMsgBoxDb` die Rollenzuordnung geprueft: - -```sql -SELECT - member_principal.name AS member_name, - role_principal.name AS role_name -FROM sys.database_role_members AS drm -JOIN sys.database_principals AS role_principal - ON role_principal.principal_id = drm.role_principal_id -JOIN sys.database_principals AS member_principal - ON member_principal.principal_id = drm.member_principal_id -WHERE member_principal.name = N'BEW\' - AND role_principal.name = N'BTS_OPERATORS'; -``` - -Liefert die Abfrage keinen passenden Eintrag, wird die fehlende Gruppenabbildung mit der BizTalk-Konfiguration verglichen und durch BizTalk-/SQL-Administration fuer die konfigurierte Gruppe repariert. Kein Einzel-Login fuer `BEW\AV23AGPWBIO1$`, keine ad-hoc Rollenvergabe und kein `sysadmin` als Umgehung. - -Entscheidungsmatrix: - -| Beobachtung | Bewertung und Massnahme | -| --- | --- | -| Alle sechs stabilen Services liefern plausible Werte | Keine Aenderung erforderlich. | -| `root\MicrosoftBizTalkServer` ist nicht erreichbar | BizTalk-WMI-Provider, WMI-Dienst, Namespace und dessen ACL gezielt pruefen. | -| `Wmi/Schema` beziehungsweise `InvalidClass`/`InvalidQuery` | Klassen- und Property-Namen gegen das installierte BizTalk-WMI-Schema pruefen; keine Berechtigungen erweitern. | -| `BizTalk SQL Access` meldet `Sql/Permission` | Maschinenkonto in die konfigurierte BizTalk-Operator-Gruppe aufnehmen, Kerberos erneuern und Agent-Dump wiederholen. | -| `BizTalk SQL Access` meldet `Sql/Connectivity` | SQL-Server-/Instanzname, DNS, Dienst, TCP-Port und Firewall pruefen. | -| `BizTalk SQL Access` meldet `Sql/Timeout` | SQL-/Netzwerkauslastung untersuchen; Timeout nur nach Ursachenanalyse anpassen. | -| SQL-gestuetzte Klassen melden `0x80131904` oder `Login failed for user` | Das exakt genannte Computerkonto in die konfigurierte BizTalk-Operator-Gruppe aufnehmen. | -| Nur `BizTalk Event Log` ist `UNKNOWN` | Lokalen Zugriff auf das Application Event Log pruefen. | -| Fehler bleibt nach Operator-Zuweisung bestehen | Exakte WMI-Klasse anhand der Service-Ausgabe bestimmen und deren BizTalk-/SQL-Rollenanforderung pruefen. | - -### Sicherheitsauswirkung und gMSA-Alternative - -Die Mitgliedschaft des Computerkontos in der Operator-Gruppe gilt fuer alle Dienste, die auf dem BizTalk-Server als `LocalSystem` laufen und mit dem Computerkonto auf Netzwerkressourcen zugreifen. Diese Auswirkung muss mit Security und BizTalk-Betrieb abgestimmt werden. - -Falls das Computerkonto keine BizTalk-Rechte erhalten darf, kann ein separater Collector unter einem dedizierten gMSA- oder Dienstkonto mit Operator-Rechten die Checkmk-Ausgabe erzeugen und als Spool-Datei bereitstellen. Das waere eine eigene Betriebsvariante und ist in der aktuellen Local-Check-Version noch nicht implementiert. Den gesamten Checkmk-Agent-Dienst nur fuer dieses Plugin auf ein anderes Konto umzustellen ist nicht empfohlen, da die Identitaetsaenderung alle Agent-Sektionen und Local Checks betrifft. - -Quellen: - -- Checkmk Windows Agent: https://docs.checkmk.com/latest/en/agent_windows.html -- Checkmk Local Checks: https://docs.checkmk.com/latest/en/localchecks.html -- BizTalk Minimum Security User Rights: https://learn.microsoft.com/en-us/biztalk/core/minimum-security-user-rights -- BizTalk Access Control and Data Security: https://learn.microsoft.com/en-us/biztalk/core/access-control-and-data-security -- BizTalk `MSBTS_GroupSetting`: https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-groupsetting-wmi -- BizTalk Windows-Gruppen und SQL-Rollen: https://learn.microsoft.com/en-us/biztalk/core/windows-groups-and-user-accounts-in-biztalk-server -- BizTalk administrative Rollen: https://learn.microsoft.com/en-us/biztalk/core/access-control-for-administrative-roles -- BizTalk Gruppen-Eigenschaften: https://learn.microsoft.com/en-us/biztalk/core/how-to-modify-group-properties -- BizTalk WMI Core Server Classes: https://learn.microsoft.com/en-us/biztalk/core/technical-reference/core-server-classes -- Microsoft LocalSystem und Computerkonten: https://learn.microsoft.com/en-us/entra/architecture/service-accounts-computer -- Microsoft `Add-ADGroupMember`: https://learn.microsoft.com/en-us/powershell/module/activedirectory/add-adgroupmember -- Microsoft `klist`: https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/klist -- Microsoft WMI Namespace Security: https://learn.microsoft.com/en-us/windows/win32/wmisdk/access-to-wmi-namespaces - -## Empfohlene Host-Struktur - -Host-Tags oder Ordner: - -- `env:ACC` -- `env:DEV` -- `env:TST` -- `env:PRD` -- `app:biztalk` - -Service-Filter fuer Views: +Servicefilter: ```text Service starts with: BizTalk ``` -Dashboard-Kacheln: +## Bakery-/Softwareverteilung -- Host/Service state fuer BizTalk-Host -- Service state fuer `BizTalk Suspended Instances` -- Service state fuer `BizTalk SQL Access` -- Graph `biztalk_suspended_total` -- Graph `biztalk_host_instances_stopped` -- Graph `biztalk_eventlog_errors` -- MSSQL-Services des SQL-Servers daneben +Die komplette Installation umfasst mehr als eine Dateiablage: + +- Programmdateien unter `%ProgramFiles%`, +- Runtimeverzeichnisse und ACLs, +- Scheduled Task mit Providerkonto, +- Local-Check-Wrapper. + +Der Wrapper allein kann per Agent Bakery verteilt werden, ersetzt aber nicht +die lokale Providerinstallation und die Kontofreigabe. Fuer den ersten Rollout +ist das signierte/abgenommene Deployment-Paket mit administrativer +Installationsautomation die klarere Variante. + +## Healthchecks + +```powershell +Get-ScheduledTaskInfo -TaskName 'BizTalk Checkmk Pulse Provider' +Get-Item "$env:ProgramData\BizTalkCheckmkPulse\data\biztalk-checkmk-pulse.snapshot" +Get-Content "$env:ProgramData\BizTalkCheckmkPulse\logs\*.log" -Tail 50 +& "$env:ProgramFiles\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe" --consume +``` + +Soll: + +- Task laeuft jede Minute und endet mit `0`. +- Snapshot ist kleiner als `SnapshotMaxBytes` und juenger als 180 Sekunden. +- Log nennt das dedizierte Providerkonto. +- Platform zeigt `read_only_group=...`. +- SQL Access zeigt `targets=2`, `available=2`. +- keine berechtigungsbedingten `UNKNOWN`-Services. + +## Alarmierung der Transportkette + +Ein Ausfall des Providers wird ueber alle sechs Services als `UNKNOWN` +sichtbar. Die Summary nennt fehlenden, unlesbaren, ungueltigen oder stale +Snapshot. Als Betriebsregel sollte `UNKNOWN` dieser Services genauso +eskaliert werden wie ein technischer Monitoringausfall. + +Optional kann Windows Task Scheduler zusaetzlich durch vorhandene Checkmk +Task-/Event-Log-Regeln ueberwacht werden. Das ist eine Ergaenzung, kein Ersatz +fuer die eingebaute Stale-Pruefung. + +## Rollout + +Empfohlene Reihenfolge: + +1. ACC: Berechtigung, Task, Snapshot, Stale-Test und Discovery abnehmen. +2. DEV/TST: gleiche Automatisierung und umgebungsspezifische Config. +3. PRD: Change, Wartungsfenster, Healthcheck und fachliche Plausibilitaet. + +Ausfuehrliche Architektur, ACL, Fehlerbilder und Abnahmekriterien: +[Dokumentation.md](../Dokumentation.md). diff --git a/scripts/package-release.cmd b/scripts/package-release.cmd index 1940db2..7fa7c30 100644 --- a/scripts/package-release.cmd +++ b/scripts/package-release.cmd @@ -6,15 +6,16 @@ if errorlevel 1 exit /b 1 set "ROOT=%~dp0.." set "OUT=%ROOT%\artifacts\BizTalkCheckmkPulse-deploy" -set "APP=%OUT%\BizTalkCheckmkPulse" +set "APP=%OUT%\application" if exist "%OUT%" rmdir /s /q "%OUT%" mkdir "%APP%" copy "%ROOT%\deployment\checkmk\biztalk_checkmk_pulse.cmd" "%OUT%\" >nul +copy "%ROOT%\deployment\Install-BizTalkCheckmkPulse.ps1" "%OUT%\" >nul +copy "%ROOT%\deployment\Uninstall-BizTalkCheckmkPulse.ps1" "%OUT%\" >nul copy "%ROOT%\src\BizTalkCheckmkPulse\bin\Release\BizTalkCheckmkPulse.exe" "%APP%\" >nul copy "%ROOT%\src\BizTalkCheckmkPulse\bin\Release\BizTalkCheckmkPulse.exe.config" "%APP%\" >nul echo Deployment package written to %OUT% exit /b 0 - diff --git a/src/BizTalkCheckmkPulse/App.config b/src/BizTalkCheckmkPulse/App.config index 2f701d6..c2534f6 100644 --- a/src/BizTalkCheckmkPulse/App.config +++ b/src/BizTalkCheckmkPulse/App.config @@ -6,8 +6,16 @@ + + + + + + + + - + diff --git a/src/BizTalkCheckmkPulse/BizTalkCheckmkPulse.csproj b/src/BizTalkCheckmkPulse/BizTalkCheckmkPulse.csproj index c561437..28a58fd 100644 --- a/src/BizTalkCheckmkPulse/BizTalkCheckmkPulse.csproj +++ b/src/BizTalkCheckmkPulse/BizTalkCheckmkPulse.csproj @@ -43,10 +43,12 @@ + + diff --git a/src/BizTalkCheckmkPulse/CheckmkLocalFormatter.cs b/src/BizTalkCheckmkPulse/CheckmkLocalFormatter.cs index bfec38c..f551d5f 100644 --- a/src/BizTalkCheckmkPulse/CheckmkLocalFormatter.cs +++ b/src/BizTalkCheckmkPulse/CheckmkLocalFormatter.cs @@ -83,6 +83,22 @@ namespace BizTalkCheckmkPulse yield return BuildLine(CheckState.Unknown, _options.ServiceName("Event Log"), "-", action); } + /// + /// Liefert stabile UNKNOWN-Services, wenn der Consumer keinen gueltigen Provider-Snapshot lesen kann. + /// + public IEnumerable FormatSnapshotFailure(string reason) + { + var detail = "Privilegierter BizTalk-Datensnapshot nicht verfuegbar: " + + SanitizeDetail(reason) + + " Massnahme: Scheduled Task 'BizTalk Checkmk Pulse Provider', Provider-Log, Snapshot-Alter und ACL pruefen."; + yield return BuildLine(CheckState.Unknown, _options.ServiceName("Platform"), "-", detail); + yield return BuildLine(CheckState.Unknown, _options.ServiceName("SQL Access"), "-", detail); + yield return BuildLine(CheckState.Unknown, _options.ServiceName("Suspended Instances"), "-", detail); + yield return BuildLine(CheckState.Unknown, _options.ServiceName("Host Instances"), "-", detail); + yield return BuildLine(CheckState.Unknown, _options.ServiceName("Runtime Artifacts"), "-", detail); + yield return BuildLine(CheckState.Unknown, _options.ServiceName("Event Log"), "-", detail); + } + /// /// Formatiert Erreichbarkeit und Basisdaten des BizTalk-WMI-Providers. /// @@ -96,6 +112,7 @@ namespace BizTalkCheckmkPulse detail.Append(available ? "BizTalk WMI and platform data reachable" : "BizTalk WMI or required platform data not readable"); detail.Append(", server=").Append(EmptyAsUnknown(result.Platform.ServerName)); AppendOptional(detail, "group", result.Platform.GroupName); + AppendOptional(detail, "read_only_group", result.Platform.ReadOnlyUserGroup); AppendOptional(detail, "operator_group", result.Platform.OperatorGroup); AppendOptional(detail, "mgmt_db", JoinDb(result.Platform.ManagementDbServer, result.Platform.ManagementDbName)); AppendOptional(detail, "msgbox_db", JoinDb(result.Platform.MessageBoxDbServer, result.Platform.MessageBoxDbName)); @@ -307,7 +324,7 @@ namespace BizTalkCheckmkPulse CheckState.Unknown, _options.ServiceName("Event Log"), "-", - "Application Event Log konnte nicht gelesen werden: " + result.EventLog.Failure + " Massnahme: lokalen Event-Log-Zugriff des Checkmk-Agentkontos und den Windows Event Log Dienst pruefen."); + "Application Event Log konnte nicht gelesen werden: " + result.EventLog.Failure + " Massnahme: lokalen Event-Log-Zugriff des Provider-Kontos und den Windows Event Log Dienst pruefen."); } var state = _options.EventLogCritThreshold > 0 && result.EventLog.Errors >= _options.EventLogCritThreshold diff --git a/src/BizTalkCheckmkPulse/FileLogger.cs b/src/BizTalkCheckmkPulse/FileLogger.cs new file mode 100644 index 0000000..39d51b5 --- /dev/null +++ b/src/BizTalkCheckmkPulse/FileLogger.cs @@ -0,0 +1,150 @@ +using System; +using System.Globalization; +using System.IO; +using System.Security.Principal; +using System.Text; +using System.Threading; + +namespace BizTalkCheckmkPulse +{ + /// + /// Kleine, ausfallsichere Tagesdatei-Protokollierung ohne externe Abhaengigkeiten. + /// Logging-Fehler duerfen weder Provider noch Checkmk-Ausgabe blockieren. + /// + internal sealed class FileLogger + { + private readonly string _directory; + private readonly int _retentionDays; + private readonly string _component; + + public FileLogger(string directory, int retentionDays, string component) + { + _directory = directory; + _retentionDays = retentionDays; + _component = string.IsNullOrWhiteSpace(component) ? "application" : component; + } + + public void Info(string message) + { + Write("INFO", message, null); + } + + public void Warning(string message) + { + Write("WARN", message, null); + } + + public void Error(string message, Exception exception) + { + Write("ERROR", message, exception); + } + + public void Prune() + { + try + { + if (!Directory.Exists(_directory)) + { + return; + } + + var cutoff = DateTime.UtcNow.Date.AddDays(-_retentionDays); + foreach (var file in Directory.GetFiles(_directory, "biztalk-checkmk-pulse-*.log")) + { + try + { + if (File.GetLastWriteTimeUtc(file) < cutoff) + { + File.Delete(file); + } + } + catch (IOException) + { + } + catch (UnauthorizedAccessException) + { + } + } + } + catch (IOException) + { + } + catch (UnauthorizedAccessException) + { + } + } + + private void Write(string level, string message, Exception exception) + { + try + { + Directory.CreateDirectory(_directory); + var path = Path.Combine( + _directory, + "biztalk-checkmk-pulse-" + DateTime.UtcNow.ToString("yyyyMMdd", CultureInfo.InvariantCulture) + ".log"); + var line = new StringBuilder() + .Append(DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture)) + .Append(" level=").Append(level) + .Append(" component=").Append(_component) + .Append(" pid=").Append(System.Diagnostics.Process.GetCurrentProcess().Id) + .Append(" identity=").Append(CurrentIdentity()) + .Append(" message=").Append(SingleLine(message)); + + if (exception != null) + { + line.Append(" exception=").Append(SingleLine(exception.ToString())); + } + + line.Append(Environment.NewLine); + var bytes = new UTF8Encoding(false).GetBytes(line.ToString()); + for (var attempt = 0; attempt < 3; attempt++) + { + try + { + using (var stream = new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.Read)) + { + stream.Write(bytes, 0, bytes.Length); + stream.Flush(); + } + + return; + } + catch (IOException) + { + if (attempt == 2) + { + return; + } + + Thread.Sleep(25 * (attempt + 1)); + } + } + } + catch (Exception) + { + // Logging bleibt bewusst best effort. + } + } + + private static string CurrentIdentity() + { + try + { + var identity = WindowsIdentity.GetCurrent(); + return identity == null ? "(unknown)" : SingleLine(identity.Name); + } + catch (Exception) + { + return "(unknown)"; + } + } + + private static string SingleLine(string value) + { + return (value ?? string.Empty) + .Replace("\r", "\\r") + .Replace("\n", "\\n") + .Replace("\t", " "); + } + } +} diff --git a/src/BizTalkCheckmkPulse/Models.cs b/src/BizTalkCheckmkPulse/Models.cs index 46ddde0..8229c8f 100644 --- a/src/BizTalkCheckmkPulse/Models.cs +++ b/src/BizTalkCheckmkPulse/Models.cs @@ -125,6 +125,7 @@ namespace BizTalkCheckmkPulse public string ServerName { get; set; } public string GroupName { get; set; } public string OperatorGroup { get; set; } + public string ReadOnlyUserGroup { get; set; } public string ManagementDbServer { get; set; } public string ManagementDbName { get; set; } public string MessageBoxDbServer { get; set; } diff --git a/src/BizTalkCheckmkPulse/MonitoringOptions.cs b/src/BizTalkCheckmkPulse/MonitoringOptions.cs index e00b9ed..a0407ab 100644 --- a/src/BizTalkCheckmkPulse/MonitoringOptions.cs +++ b/src/BizTalkCheckmkPulse/MonitoringOptions.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Configuration; using System.Globalization; +using System.IO; using System.Linq; namespace BizTalkCheckmkPulse @@ -27,6 +28,12 @@ namespace BizTalkCheckmkPulse public int EventLogWarnThreshold { get; set; } public int EventLogCritThreshold { get; set; } public IReadOnlyList EventLogSources { get; set; } + public string SnapshotPath { get; set; } + public int SnapshotMaxAgeSeconds { get; set; } + public int SnapshotMaxBytes { get; set; } + public string LogDirectory { get; set; } + public int LogRetentionDays { get; set; } + public bool Collect { get; set; } public bool SelfTest { get; set; } /// @@ -50,6 +57,12 @@ namespace BizTalkCheckmkPulse EventLogWarnThreshold = 1; EventLogCritThreshold = 10; EventLogSources = new[] { "BizTalk Server", "XLANG/s", "ENTSSO", "BizTalk Server Application", "BizTalk Server EDI" }; + var commonData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData); + SnapshotPath = Path.Combine(commonData, "BizTalkCheckmkPulse", "data", "biztalk-checkmk-pulse.snapshot"); + SnapshotMaxAgeSeconds = 180; + SnapshotMaxBytes = 1048576; + LogDirectory = Path.Combine(commonData, "BizTalkCheckmkPulse", "logs"); + LogRetentionDays = 30; } /// @@ -91,8 +104,17 @@ namespace BizTalkCheckmkPulse options.EventLogWarnThreshold = ReadInt(settings, "EventLogWarnThreshold", options.EventLogWarnThreshold, 0, 1000000); options.EventLogCritThreshold = ReadInt(settings, "EventLogCritThreshold", options.EventLogCritThreshold, 0, 1000000); options.EventLogSources = ReadSources(ReadString(settings, "EventLogSources", string.Join("|", options.EventLogSources))); + options.SnapshotPath = Environment.ExpandEnvironmentVariables(ReadString(settings, "SnapshotPath", options.SnapshotPath)); + options.SnapshotMaxAgeSeconds = ReadInt(settings, "SnapshotMaxAgeSeconds", options.SnapshotMaxAgeSeconds, 60, 86400); + options.SnapshotMaxBytes = ReadInt(settings, "SnapshotMaxBytes", options.SnapshotMaxBytes, 4096, 16777216); + options.LogDirectory = Environment.ExpandEnvironmentVariables(ReadString(settings, "LogDirectory", options.LogDirectory)); + options.LogRetentionDays = ReadInt(settings, "LogRetentionDays", options.LogRetentionDays, 1, 365); ApplyArguments(options, args ?? new string[0]); + if (!options.SelfTest) + { + Validate(options); + } return options; } @@ -110,6 +132,14 @@ namespace BizTalkCheckmkPulse { options.SelfTest = true; } + else if (EqualsAny(arg, "--collect", "/collect")) + { + options.Collect = true; + } + else if (EqualsAny(arg, "--consume", "/consume")) + { + options.Collect = false; + } else if (EqualsAny(arg, "--server", "/server") && i + 1 < args.Length) { options.Server = args[++i]; @@ -133,6 +163,22 @@ namespace BizTalkCheckmkPulse } } + /// + /// Verhindert unsichere oder mehrdeutige Laufzeitpfade. + /// + private static void Validate(MonitoringOptions options) + { + if (string.IsNullOrWhiteSpace(options.SnapshotPath) || !Path.IsPathRooted(options.SnapshotPath)) + { + throw new ConfigurationErrorsException("SnapshotPath must be an absolute path."); + } + + if (string.IsNullOrWhiteSpace(options.LogDirectory) || !Path.IsPathRooted(options.LogDirectory)) + { + throw new ConfigurationErrorsException("LogDirectory must be an absolute path."); + } + } + /// /// Liest einen getrimmten Konfigurationswert. /// diff --git a/src/BizTalkCheckmkPulse/Program.cs b/src/BizTalkCheckmkPulse/Program.cs index 157bba7..02b1690 100644 --- a/src/BizTalkCheckmkPulse/Program.cs +++ b/src/BizTalkCheckmkPulse/Program.cs @@ -1,19 +1,22 @@ using System; -using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Security.Principal; using System.Text; namespace BizTalkCheckmkPulse { /// - /// Einstiegspunkt des Checkmk Local Checks. + /// Einstiegspunkt fuer privilegierten Provider und unprivilegierten Checkmk-Consumer. /// internal static class Program { /// - /// Laedt die Konfiguration, fuehrt die Probes aus und schreibt Checkmk-Zeilen nach STDOUT. + /// Laedt die Konfiguration und fuehrt je nach Modus Provider oder Snapshot-Consumer aus. /// /// Kommandozeilenargumente wie --self-test oder --environment. - /// Immer 0, damit Diagnosefehler als UNKNOWN-Service statt als kaputte Agent-Sektion erscheinen. + /// Consumer immer 0; Provider 0 bei Erfolg und ungleich 0 bei einem Laufzeitfehler. private static int Main(string[] args) { MonitoringOptions options = null; @@ -34,38 +37,151 @@ namespace BizTalkCheckmkPulse return 0; } - var result = new ProbeResult(); - var wmiProbe = new WmiBizTalkProbe(options); - wmiProbe.Query(result); - - // Der SQL-Test verwendet bewusst dieselbe Windows-Identitaet wie der Checkmk-Agent. - var sqlProbe = new SqlConnectivityProbe(options); - sqlProbe.Query(result); - - if (options.ProbeEventLog) - { - var eventLogProbe = new EventLogProbe(options); - eventLogProbe.Query(result); - } - - foreach (var line in formatter.Format(result)) - { - Console.WriteLine(line); - } - - return 0; + return options.Collect + ? RunProvider(options, formatter) + : RunConsumer(options, formatter); } catch (Exception ex) { var fallbackOptions = options ?? new MonitoringOptions(); var formatter = new CheckmkLocalFormatter(fallbackOptions); - foreach (var line in formatter.FormatFatal("BizTalk Checkmk Pulse failed: " + ex.GetType().Name + ": " + ex.Message)) + foreach (var line in formatter.FormatSnapshotFailure( + "Programmstart fehlgeschlagen: " + ex.GetType().Name + ": " + ex.Message)) + { + Console.WriteLine(line); + } + + return options != null && options.Collect ? 1 : 0; + } + } + + private static int RunProvider(MonitoringOptions options, CheckmkLocalFormatter formatter) + { + var logger = new FileLogger(options.LogDirectory, options.LogRetentionDays, "provider"); + logger.Prune(); + var stopwatch = Stopwatch.StartNew(); + var lockPath = options.SnapshotPath + ".provider.lock"; + + try + { + EnsureProviderIdentity(); + Directory.CreateDirectory(Path.GetDirectoryName(options.SnapshotPath)); + using (new FileStream(lockPath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None)) + { + logger.Info("Collection started. snapshot=" + options.SnapshotPath); + var result = Collect(options); + var lines = formatter.Format(result).ToArray(); + new SnapshotStore(options.SnapshotPath, options.SnapshotMaxBytes) + .Write(lines, DateTime.UtcNow, CurrentIdentity()); + stopwatch.Stop(); + logger.Info( + "Collection completed. lines=" + + lines.Length + + " diagnostics=" + + result.Diagnostics.Count + + " elapsed_ms=" + + stopwatch.ElapsedMilliseconds); + return 0; + } + } + catch (IOException ex) + { + logger.Warning("Collection skipped or snapshot I/O failed: " + ex.Message); + return 2; + } + catch (Exception ex) + { + stopwatch.Stop(); + logger.Error("Collection failed after " + stopwatch.ElapsedMilliseconds + " ms.", ex); + TryWriteFatalSnapshot(options, formatter, ex, logger); + return 1; + } + } + + private static void EnsureProviderIdentity() + { + using (var identity = WindowsIdentity.GetCurrent()) + { + if (identity != null + && identity.User != null + && identity.User.IsWellKnown(WellKnownSidType.LocalSystemSid)) + { + throw new InvalidOperationException( + "Provider mode must not run as LocalSystem. Configure the dedicated Scheduled Task account."); + } + } + } + + private static int RunConsumer(MonitoringOptions options, CheckmkLocalFormatter formatter) + { + var snapshot = new SnapshotStore(options.SnapshotPath, options.SnapshotMaxBytes) + .Read(DateTime.UtcNow, TimeSpan.FromSeconds(options.SnapshotMaxAgeSeconds)); + if (snapshot.IsSuccess) + { + foreach (var line in snapshot.Lines) { Console.WriteLine(line); } return 0; } + + new FileLogger(options.LogDirectory, options.LogRetentionDays, "consumer") + .Warning("Snapshot rejected. reason=" + snapshot.Error + " path=" + options.SnapshotPath); + foreach (var line in formatter.FormatSnapshotFailure(snapshot.Error)) + { + Console.WriteLine(line); + } + + return 0; + } + + private static ProbeResult Collect(MonitoringOptions options) + { + var result = new ProbeResult(); + new WmiBizTalkProbe(options).Query(result); + new SqlConnectivityProbe(options).Query(result); + + if (options.ProbeEventLog) + { + new EventLogProbe(options).Query(result); + } + + return result; + } + + private static void TryWriteFatalSnapshot( + MonitoringOptions options, + CheckmkLocalFormatter formatter, + Exception exception, + FileLogger logger) + { + try + { + var lines = formatter + .FormatFatal("Privilegierter Datenprovider fehlgeschlagen: " + exception.GetType().Name + ": " + exception.Message) + .ToArray(); + new SnapshotStore(options.SnapshotPath, options.SnapshotMaxBytes) + .Write(lines, DateTime.UtcNow, CurrentIdentity()); + logger.Warning("A current UNKNOWN snapshot was written after the provider failure."); + } + catch (Exception snapshotException) + { + logger.Error("Fatal UNKNOWN snapshot could not be written.", snapshotException); + } + } + + private static string CurrentIdentity() + { + try + { + var identity = WindowsIdentity.GetCurrent(); + return identity == null ? "(unknown)" : identity.Name; + } + catch (Exception) + { + return "(unknown)"; + } } } } diff --git a/src/BizTalkCheckmkPulse/Properties/AssemblyInfo.cs b/src/BizTalkCheckmkPulse/Properties/AssemblyInfo.cs index 1729e66..21ca04c 100644 --- a/src/BizTalkCheckmkPulse/Properties/AssemblyInfo.cs +++ b/src/BizTalkCheckmkPulse/Properties/AssemblyInfo.cs @@ -1,3 +1,10 @@ using System.Runtime.CompilerServices; +using System.Reflection; [assembly: InternalsVisibleTo("BizTalkCheckmkPulse.Tests")] +[assembly: AssemblyTitle("BizTalk Checkmk Pulse")] +[assembly: AssemblyDescription("Privileged BizTalk data provider and validated Checkmk snapshot consumer")] +[assembly: AssemblyCompany("BEW")] +[assembly: AssemblyProduct("BizTalk Checkmk Pulse")] +[assembly: AssemblyVersion("2.0.0.0")] +[assembly: AssemblyFileVersion("2.0.0.0")] diff --git a/src/BizTalkCheckmkPulse/SnapshotStore.cs b/src/BizTalkCheckmkPulse/SnapshotStore.cs new file mode 100644 index 0000000..b4120b7 --- /dev/null +++ b/src/BizTalkCheckmkPulse/SnapshotStore.cs @@ -0,0 +1,353 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Diagnostics; + +namespace BizTalkCheckmkPulse +{ + /// + /// Schreibt und liest einen atomaren, integritaetsgeschuetzten Checkmk-Snapshot. + /// + internal sealed class SnapshotStore + { + internal const string Magic = "BIZTALK_CHECKMK_PULSE_SNAPSHOT_V1"; + private static readonly UTF8Encoding StrictUtf8 = new UTF8Encoding(false, true); + private readonly string _path; + private readonly int _maxBytes; + + public SnapshotStore(string path, int maxBytes) + { + _path = path; + _maxBytes = maxBytes; + } + + public void Write(IReadOnlyCollection lines, DateTime generatedUtc, string identity) + { + ValidatePayload(lines); + var payload = string.Join("\n", lines) + "\n"; + var payloadBytes = StrictUtf8.GetBytes(payload); + var content = BuildHeader(generatedUtc, identity, lines.Count, Hash(payloadBytes)) + payload; + var contentBytes = StrictUtf8.GetBytes(content); + if (contentBytes.Length > _maxBytes) + { + throw new InvalidDataException("Snapshot exceeds configured SnapshotMaxBytes."); + } + + var directory = Path.GetDirectoryName(_path); + if (string.IsNullOrWhiteSpace(directory)) + { + throw new InvalidOperationException("Snapshot path has no parent directory."); + } + + Directory.CreateDirectory(directory); + var temporaryPath = Path.Combine( + directory, + Path.GetFileName(_path) + "." + Process.GetCurrentProcess().Id.ToString(CultureInfo.InvariantCulture) + "." + Guid.NewGuid().ToString("N") + ".tmp"); + + try + { + using (var stream = new FileStream( + temporaryPath, + FileMode.CreateNew, + FileAccess.Write, + FileShare.None, + 4096, + FileOptions.WriteThrough)) + { + stream.Write(contentBytes, 0, contentBytes.Length); + stream.Flush(true); + } + + if (File.Exists(_path)) + { + File.Replace(temporaryPath, _path, null, true); + } + else + { + File.Move(temporaryPath, _path); + } + } + finally + { + try + { + if (File.Exists(temporaryPath)) + { + File.Delete(temporaryPath); + } + } + catch (IOException) + { + } + } + } + + public SnapshotReadResult Read(DateTime utcNow, TimeSpan maximumAge) + { + try + { + var bytes = ReadBytesWithRetry(); + var text = StrictUtf8.GetString(bytes).Replace("\r\n", "\n"); + var separator = text.IndexOf("\n\n", StringComparison.Ordinal); + if (separator < 0) + { + return SnapshotReadResult.Failed("Snapshot header separator is missing."); + } + + var headerLines = text.Substring(0, separator).Split('\n'); + if (headerLines.Length != 6 || !string.Equals(headerLines[0], Magic, StringComparison.Ordinal)) + { + return SnapshotReadResult.Failed("Snapshot format or version is invalid."); + } + + DateTime generatedUtc; + int expectedLineCount; + if (!TryReadDate(headerLines[1], "generatedUtc=", out generatedUtc) + || !TryReadInt(headerLines[4], "payloadLines=", out expectedLineCount)) + { + return SnapshotReadResult.Failed("Snapshot metadata is invalid."); + } + + var expectedMachine = DecodeHeader(headerLines[2], "machineBase64="); + DecodeHeader(headerLines[3], "identityBase64="); + var expectedHash = ReadHeaderValue(headerLines[5], "payloadSha256="); + if (!string.Equals(expectedMachine, Environment.MachineName, StringComparison.OrdinalIgnoreCase)) + { + return SnapshotReadResult.Failed("Snapshot was created for a different machine."); + } + + var payload = text.Substring(separator + 2); + if (!payload.EndsWith("\n", StringComparison.Ordinal) + || payload.IndexOf("\n\n", StringComparison.Ordinal) >= 0) + { + return SnapshotReadResult.Failed("Snapshot payload framing is invalid."); + } + + var payloadBytes = StrictUtf8.GetBytes(payload); + if (!FixedTimeEquals(expectedHash, Hash(payloadBytes))) + { + return SnapshotReadResult.Failed("Snapshot SHA-256 validation failed."); + } + + var lines = payload + .Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries) + .ToArray(); + if (lines.Length != expectedLineCount) + { + return SnapshotReadResult.Failed("Snapshot payload line count is invalid."); + } + + ValidatePayload(lines); + var age = utcNow - generatedUtc; + if (age < TimeSpan.FromMinutes(-5)) + { + return SnapshotReadResult.Failed("Snapshot timestamp is too far in the future."); + } + + if (age > maximumAge) + { + return SnapshotReadResult.Failed( + "Snapshot is stale: age=" + + Math.Floor(age.TotalSeconds).ToString(CultureInfo.InvariantCulture) + + "s, maximum=" + + Math.Floor(maximumAge.TotalSeconds).ToString(CultureInfo.InvariantCulture) + + "s."); + } + + return SnapshotReadResult.Success(lines, generatedUtc); + } + catch (FileNotFoundException) + { + return SnapshotReadResult.Failed("Snapshot file does not exist."); + } + catch (DirectoryNotFoundException) + { + return SnapshotReadResult.Failed("Snapshot directory does not exist."); + } + catch (UnauthorizedAccessException ex) + { + return SnapshotReadResult.Failed("Snapshot cannot be read: " + ex.Message); + } + catch (IOException ex) + { + return SnapshotReadResult.Failed("Snapshot I/O failed: " + ex.Message); + } + catch (Exception ex) + { + return SnapshotReadResult.Failed("Snapshot validation failed: " + ex.GetType().Name + ": " + ex.Message); + } + } + + private byte[] ReadBytesWithRetry() + { + for (var attempt = 0; ; attempt++) + { + try + { + using (var stream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete)) + { + if (stream.Length <= 0 || stream.Length > _maxBytes) + { + throw new InvalidDataException("Snapshot size is outside the allowed range."); + } + + var bytes = new byte[(int)stream.Length]; + var offset = 0; + while (offset < bytes.Length) + { + var read = stream.Read(bytes, offset, bytes.Length - offset); + if (read == 0) + { + throw new EndOfStreamException("Unexpected end of snapshot."); + } + + offset += read; + } + + return bytes; + } + } + catch (IOException) + { + if (attempt >= 2) + { + throw; + } + + Thread.Sleep(25 * (attempt + 1)); + } + } + } + + private static string BuildHeader(DateTime generatedUtc, string identity, int lineCount, string hash) + { + return Magic + "\n" + + "generatedUtc=" + generatedUtc.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture) + "\n" + + "machineBase64=" + Convert.ToBase64String(StrictUtf8.GetBytes(Environment.MachineName)) + "\n" + + "identityBase64=" + Convert.ToBase64String(StrictUtf8.GetBytes(identity ?? string.Empty)) + "\n" + + "payloadLines=" + lineCount.ToString(CultureInfo.InvariantCulture) + "\n" + + "payloadSha256=" + hash + "\n\n"; + } + + private static void ValidatePayload(IEnumerable lines) + { + if (lines == null) + { + throw new ArgumentNullException("lines"); + } + + var count = 0; + foreach (var line in lines) + { + count++; + if (string.IsNullOrWhiteSpace(line) + || line.IndexOf('\r') >= 0 + || line.IndexOf('\n') >= 0 + || line.Length < 5 + || line[1] != ' ' + || line[2] != '"' + || line[0] < '0' + || line[0] > '3') + { + throw new InvalidDataException("Snapshot contains an invalid Checkmk local-check line."); + } + } + + if (count < 6) + { + throw new InvalidDataException("Snapshot must contain all six stable services."); + } + } + + private static string Hash(byte[] bytes) + { + using (var sha = SHA256.Create()) + { + return string.Concat(sha.ComputeHash(bytes).Select(x => x.ToString("x2", CultureInfo.InvariantCulture))); + } + } + + private static bool FixedTimeEquals(string left, string right) + { + if (left == null || right == null || left.Length != right.Length) + { + return false; + } + + var difference = 0; + for (var i = 0; i < left.Length; i++) + { + difference |= left[i] ^ right[i]; + } + + return difference == 0; + } + + private static string ReadHeaderValue(string line, string prefix) + { + if (line == null || !line.StartsWith(prefix, StringComparison.Ordinal)) + { + throw new InvalidDataException("Missing snapshot header " + prefix); + } + + return line.Substring(prefix.Length); + } + + private static string DecodeHeader(string line, string prefix) + { + return StrictUtf8.GetString(Convert.FromBase64String(ReadHeaderValue(line, prefix))); + } + + private static bool TryReadDate(string line, string prefix, out DateTime result) + { + return DateTime.TryParseExact( + ReadHeaderValue(line, prefix), + "o", + CultureInfo.InvariantCulture, + DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, + out result); + } + + private static bool TryReadInt(string line, string prefix, out int result) + { + return int.TryParse(ReadHeaderValue(line, prefix), NumberStyles.Integer, CultureInfo.InvariantCulture, out result); + } + } + + internal sealed class SnapshotReadResult + { + private SnapshotReadResult() + { + Lines = new string[0]; + } + + public bool IsSuccess { get; private set; } + public string Error { get; private set; } + public IReadOnlyList Lines { get; private set; } + public DateTime GeneratedUtc { get; private set; } + + public static SnapshotReadResult Success(IReadOnlyList lines, DateTime generatedUtc) + { + return new SnapshotReadResult + { + IsSuccess = true, + Lines = lines, + GeneratedUtc = generatedUtc + }; + } + + public static SnapshotReadResult Failed(string error) + { + return new SnapshotReadResult + { + IsSuccess = false, + Error = error + }; + } + } +} diff --git a/src/BizTalkCheckmkPulse/SqlConnectivityProbe.cs b/src/BizTalkCheckmkPulse/SqlConnectivityProbe.cs index 49dbf07..797d20c 100644 --- a/src/BizTalkCheckmkPulse/SqlConnectivityProbe.cs +++ b/src/BizTalkCheckmkPulse/SqlConnectivityProbe.cs @@ -56,8 +56,8 @@ namespace BizTalkCheckmkPulse ? "SQL-Zielermittlung ist unvollstaendig, weil der BizTalk-WMI-Provider beim SQL-Zugriff abgewiesen wurde." : "SQL-Zielermittlung ist unvollstaendig, weil nicht alle erforderlichen BizTalk-Plattformklassen gelesen wurden.", Action = wmiPermissionFailure - ? "Zuerst die Wmi/Permission-Diagnose und die Mitgliedschaft des Computerkontos in der konfigurierten BizTalk-Operator-Gruppe beheben." - : "Zuerst den Service 'BizTalk Platform' und MSBTS_GroupSetting pruefen. Danach den Agent-Dump erneut ausfuehren." + ? "Zuerst die Wmi/Permission-Diagnose sowie Provider-Konto, konfigurierte BizTalk-Read-Only-Gruppe und BTS_READONLY_USERS pruefen." + : "Zuerst den Service 'BizTalk Platform' und MSBTS_GroupSetting im Provider-Log pruefen. Danach den Scheduled Task erneut starten." }); } @@ -112,7 +112,7 @@ namespace BizTalkCheckmkPulse Component = "Windows identity", Required = false, Summary = "Die Windows-Ausfuehrungsidentitaet konnte nicht bestimmt werden.", - Action = "Agent-Dienstkonto mit 'sc.exe qc CheckMKService' beziehungsweise in services.msc pruefen.", + Action = "Konto des Scheduled Tasks 'BizTalk Checkmk Pulse Provider' pruefen.", TechnicalDetails = ex.GetType().Name + ": " + ex.Message }); } @@ -268,7 +268,7 @@ namespace BizTalkCheckmkPulse case DiagnosticCategory.Timeout: return "SQL-Verbindungsaufbau oder Testabfrage hat das konfigurierte Zeitlimit ueberschritten."; case DiagnosticCategory.Connectivity: - return "SQL Server oder die konfigurierte SQL-Instanz ist aus dem Checkmk-Agent-Kontext nicht erreichbar."; + return "SQL Server oder die konfigurierte SQL-Instanz ist aus dem Provider-Kontext nicht erreichbar."; case DiagnosticCategory.Configuration: return "SQL-Verbindung scheitert an TLS-, Zertifikats-, SPN- oder SSPI-Konfiguration."; default: @@ -288,7 +288,7 @@ namespace BizTalkCheckmkPulse switch (category) { case DiagnosticCategory.Permission: - return networkIdentity + " in die konfigurierte BizTalk-Operator-Gruppe aufnehmen; keine direkten BizTalk-DB-Rollen vergeben. Danach Kerberos-Tickets erneuern und den Agent-Dump wiederholen."; + return "Provider-Konto " + networkIdentity + " und dessen Mitgliedschaft in der konfigurierten BizTalk-Read-Only-Gruppe sowie BTS_READONLY_USERS pruefen; keine direkten BizTalk-DB-Rollen vergeben. Danach den Scheduled Task mit neuem Anmeldetoken starten."; case DiagnosticCategory.Timeout: return "Netzwerkpfad, DNS, SQL-Port, Firewall und Auslastung fuer " + target.Server + " pruefen; Timeout nur nach Ursachenanalyse erhoehen."; case DiagnosticCategory.Connectivity: diff --git a/src/BizTalkCheckmkPulse/WmiBizTalkProbe.cs b/src/BizTalkCheckmkPulse/WmiBizTalkProbe.cs index 7e04342..8a3a003 100644 --- a/src/BizTalkCheckmkPulse/WmiBizTalkProbe.cs +++ b/src/BizTalkCheckmkPulse/WmiBizTalkProbe.cs @@ -25,7 +25,7 @@ namespace BizTalkCheckmkPulse private const int OrchestrationStarted = 4; private const int EAccessDenied = unchecked((int)0x80070005); internal const string GroupSettingQuery = - "SELECT Name, BizTalkOperatorGroup, MgmtDbServerName, MgmtDbName, SubscriptionDBServerName, SubscriptionDBName FROM MSBTS_GroupSetting"; + "SELECT Name, BizTalkOperatorGroup, BizTalkReadOnlyUserGroup, MgmtDbServerName, MgmtDbName, SubscriptionDBServerName, SubscriptionDBName FROM MSBTS_GroupSetting"; private readonly MonitoringOptions _options; /// @@ -106,6 +106,7 @@ namespace BizTalkCheckmkPulse groupFound = true; result.Platform.GroupName = FirstNonEmpty(WmiHelpers.GetString(item, "Name"), WmiHelpers.GetString(item, "MgmtDbName")); result.Platform.OperatorGroup = WmiHelpers.GetString(item, "BizTalkOperatorGroup"); + result.Platform.ReadOnlyUserGroup = WmiHelpers.GetString(item, "BizTalkReadOnlyUserGroup"); result.Platform.ManagementDbServer = WmiHelpers.GetString(item, "MgmtDbServerName"); result.Platform.ManagementDbName = WmiHelpers.GetString(item, "MgmtDbName"); result.Platform.MessageBoxDbServer = WmiHelpers.GetString(item, "SubscriptionDBServerName"); @@ -672,7 +673,7 @@ namespace BizTalkCheckmkPulse return "BizTalk-WMI-Provider konnte den SQL-Zugriff fuer " + sqlLoginPrincipal + " nicht anmelden."; } - return "Zugriff auf " + component + " wurde im LocalSystem-Kontext verweigert."; + return "Zugriff des privilegierten Provider-Kontos auf " + component + " wurde verweigert."; case DiagnosticCategory.Configuration: return connectionFailure ? "BizTalk-WMI-Namespace ist nicht vorhanden oder nicht korrekt registriert." @@ -704,13 +705,13 @@ namespace BizTalkCheckmkPulse case DiagnosticCategory.Permission: if (!string.IsNullOrWhiteSpace(sqlLoginPrincipal)) { - return "Computerkonto " + sqlLoginPrincipal - + " der in der BizTalk-Gruppe konfigurierten BizTalk-Operator-Gruppe zuordnen; keine direkten SQL-Logins oder Datenbankrollen vergeben."; + return "Provider-Konto " + sqlLoginPrincipal + + " und dessen Mitgliedschaft in der konfigurierten BizTalk-Read-Only-Gruppe sowie BTS_READONLY_USERS pruefen; keine direkten SQL-Logins oder Datenbankrollen vergeben."; } return connectionFailure - ? "Namespace-ACL fuer root\\MicrosoftBizTalkServer gezielt pruefen. LocalSystem benoetigt lokalen Lesezugriff; keine pauschalen WMI-Rechte vergeben." - : "Agent-Dump pruefen und Computerkonto \\" + Environment.MachineName + "$ zunaechst der konfigurierten BizTalk-Operator-Gruppe zuordnen."; + ? "Lokalen Namespace-Zugriff des Scheduled-Task-Kontos auf root\\MicrosoftBizTalkServer gezielt pruefen; keine pauschalen WMI-Rechte vergeben." + : "Provider-Identitaet und Mitgliedschaft in der konfigurierten BizTalk-Read-Only-Gruppe pruefen; Operator-Rechte nur nach klassenspezifischer Analyse erwaegen."; case DiagnosticCategory.Configuration: return "BizTalk-WMI-Provider/Namespace auf dem BizTalk-Server pruefen und gegebenenfalls mit dem BizTalk-Setup reparieren."; case DiagnosticCategory.Schema: diff --git a/tests/BizTalkCheckmkPulse.Tests/Program.cs b/tests/BizTalkCheckmkPulse.Tests/Program.cs index c3ecb23..31b6745 100644 --- a/tests/BizTalkCheckmkPulse.Tests/Program.cs +++ b/tests/BizTalkCheckmkPulse.Tests/Program.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Runtime.InteropServices; +using System.Text; namespace BizTalkCheckmkPulse.Tests { @@ -21,6 +23,12 @@ namespace BizTalkCheckmkPulse.Tests Run("ProviderSqlLoginFailureIsPermission", ProviderSqlLoginFailureIsPermission); Run("RejectedSqlPrincipalIsExtracted", RejectedSqlPrincipalIsExtracted); Run("PlatformPermissionPropagatesToSqlDiscovery", PlatformPermissionPropagatesToSqlDiscovery); + Run("SnapshotRoundTripPreservesLines", SnapshotRoundTripPreservesLines); + Run("SnapshotRejectsTampering", SnapshotRejectsTampering); + Run("SnapshotRejectsStaleData", SnapshotRejectsStaleData); + Run("SnapshotRejectsDifferentMachine", SnapshotRejectsDifferentMachine); + Run("SnapshotReportsMissingFile", SnapshotReportsMissingFile); + Run("SnapshotFailureEmitsStableUnknownServices", SnapshotFailureEmitsStableUnknownServices); if (Failures.Count == 0) { @@ -81,6 +89,7 @@ namespace BizTalkCheckmkPulse.Tests var query = WmiBizTalkProbe.GroupSettingQuery; Assert(query.Contains("FROM MSBTS_GroupSetting"), "MSBTS_GroupSetting query missing"); Assert(query.Contains("BizTalkOperatorGroup"), "configured operator group property missing"); + Assert(query.Contains("BizTalkReadOnlyUserGroup"), "configured read-only group property missing"); Assert(query.Contains("SubscriptionDBServerName"), "master MessageBox server property missing"); Assert(query.Contains("SubscriptionDBName"), "master MessageBox database property missing"); Assert(query.IndexOf("MessageBoxSetting", StringComparison.OrdinalIgnoreCase) < 0, "unsupported MessageBoxSetting class present"); @@ -128,6 +137,114 @@ namespace BizTalkCheckmkPulse.Tests "SQL discovery classification"); } + private static void SnapshotRoundTripPreservesLines() + { + WithTemporarySnapshot((path, store) => + { + var lines = new CheckmkLocalFormatter(new MonitoringOptions()).FormatSelfTest().ToArray(); + var generated = DateTime.UtcNow; + store.Write(lines, generated, "DOMAIN\\collector$"); + var result = store.Read(generated.AddSeconds(10), TimeSpan.FromMinutes(3)); + + Assert(result.IsSuccess, "snapshot should be readable: " + result.Error); + AssertEqual(lines.Length, result.Lines.Count, "snapshot line count"); + Assert(lines.SequenceEqual(result.Lines), "snapshot payload changed"); + + var replacement = lines.Select(x => x.Replace("Self test OK", "Replacement OK")).ToArray(); + store.Write(replacement, generated.AddSeconds(30), "DOMAIN\\collector$"); + var replaced = store.Read(generated.AddSeconds(40), TimeSpan.FromMinutes(3)); + Assert(replaced.IsSuccess, "replaced snapshot should be readable"); + Assert(replacement.SequenceEqual(replaced.Lines), "atomic replacement payload changed"); + }); + } + + private static void SnapshotRejectsTampering() + { + WithTemporarySnapshot((path, store) => + { + var lines = new CheckmkLocalFormatter(new MonitoringOptions()).FormatSelfTest().ToArray(); + store.Write(lines, DateTime.UtcNow, "DOMAIN\\collector$"); + var content = File.ReadAllText(path, Encoding.UTF8); + File.WriteAllText(path, content.Replace("Self test OK", "Tampered output"), new UTF8Encoding(false)); + + var result = store.Read(DateTime.UtcNow, TimeSpan.FromMinutes(3)); + Assert(!result.IsSuccess, "tampered snapshot must be rejected"); + Assert(result.Error.IndexOf("SHA-256", StringComparison.OrdinalIgnoreCase) >= 0, "tamper reason should mention SHA-256"); + }); + } + + private static void SnapshotRejectsStaleData() + { + WithTemporarySnapshot((path, store) => + { + var lines = new CheckmkLocalFormatter(new MonitoringOptions()).FormatSelfTest().ToArray(); + var generated = DateTime.UtcNow.AddMinutes(-10); + store.Write(lines, generated, "DOMAIN\\collector$"); + + var result = store.Read(DateTime.UtcNow, TimeSpan.FromMinutes(3)); + Assert(!result.IsSuccess, "stale snapshot must be rejected"); + Assert(result.Error.IndexOf("stale", StringComparison.OrdinalIgnoreCase) >= 0, "stale reason missing"); + }); + } + + private static void SnapshotRejectsDifferentMachine() + { + WithTemporarySnapshot((path, store) => + { + var lines = new CheckmkLocalFormatter(new MonitoringOptions()).FormatSelfTest().ToArray(); + store.Write(lines, DateTime.UtcNow, "DOMAIN\\collector$"); + var content = File.ReadAllText(path, Encoding.UTF8); + var currentMachine = Convert.ToBase64String(Encoding.UTF8.GetBytes(Environment.MachineName)); + var otherMachine = Convert.ToBase64String(Encoding.UTF8.GetBytes("OTHER-SERVER")); + File.WriteAllText( + path, + content.Replace("machineBase64=" + currentMachine, "machineBase64=" + otherMachine), + new UTF8Encoding(false)); + + var result = store.Read(DateTime.UtcNow, TimeSpan.FromMinutes(3)); + Assert(!result.IsSuccess, "snapshot for another machine must be rejected"); + Assert(result.Error.IndexOf("different machine", StringComparison.OrdinalIgnoreCase) >= 0, "machine mismatch reason missing"); + }); + } + + private static void SnapshotReportsMissingFile() + { + WithTemporarySnapshot((path, store) => + { + var result = store.Read(DateTime.UtcNow, TimeSpan.FromMinutes(3)); + Assert(!result.IsSuccess, "missing snapshot must be rejected"); + Assert(result.Error.IndexOf("does not exist", StringComparison.OrdinalIgnoreCase) >= 0, "missing file reason missing"); + }); + } + + private static void SnapshotFailureEmitsStableUnknownServices() + { + var lines = new CheckmkLocalFormatter(new MonitoringOptions()) + .FormatSnapshotFailure("Snapshot file does not exist.") + .ToArray(); + AssertEqual(6, lines.Length, "snapshot failure stable service count"); + Assert(lines.All(x => x.StartsWith("3 \"BizTalk ", StringComparison.Ordinal)), "snapshot failure must be UNKNOWN"); + Assert(lines.All(x => x.Contains("Scheduled Task")), "snapshot failure must contain provider action"); + } + + private static void WithTemporarySnapshot(Action test) + { + var directory = Path.Combine(Path.GetTempPath(), "BizTalkCheckmkPulse.Tests." + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(directory); + var path = Path.Combine(directory, "snapshot.txt"); + try + { + test(path, new SnapshotStore(path, 1048576)); + } + finally + { + if (Directory.Exists(directory)) + { + Directory.Delete(directory, true); + } + } + } + private static ProbeResult CreateSuspensionResult(string applicationName) { var result = new ProbeResult();