Stabilize BizTalk inventory collection and diagnostics
Build und Test / build (push) Has been cancelled

This commit is contained in:
2026-07-28 10:55:57 +02:00
parent 439427cc17
commit d1e10920c9
11 changed files with 1008 additions and 172 deletions
+60 -19
View File
@@ -38,13 +38,13 @@ Administrative cmd.exe
+-- SystemCollector +-- SystemCollector
| +-- Windows WMI | +-- Windows WMI
| +-- BizTalk Registry | +-- BizTalk Registry
| +-- Management SQL/DB | +-- MSBTS_GroupSetting für Management SQL/DB
| |
+-- BizTalkWmiCollector +-- BizTalkWmiCollector
| +-- ExplorerOM: vollständige Anwendungsliste
| +-- root\MicrosoftBizTalkServer | +-- root\MicrosoftBizTalkServer
| +-- Anwendungen und Artefakte | +-- optionale Artefakte, Adapter, Hosts und Handler
| +-- Adapter, Hosts und Handler | +-- optionaler SAP-Endpunkt-Fallback
| +-- SAP-Endpunkt-Fallback
| |
+-- BindingExportCollector +-- BindingExportCollector
| +-- BTSTask ExportBindings /GroupLevel | +-- BTSTask ExportBindings /GroupLevel
@@ -62,7 +62,13 @@ Administrative cmd.exe
+-- atomare Ausgabe +-- atomare Ausgabe
``` ```
Das Programm referenziert keine `Microsoft.BizTalk.*`-Assembly. Dadurch besteht das Deployment nur aus EXE, Konfiguration, Startskript und Dokumentation. Die BizTalk-Integration erfolgt über die offiziell vorhandenen Verwaltungsoberflächen WMI und `BTSTask.exe`. Das Programm besitzt keine compile-time-Referenz auf eine
`Microsoft.BizTalk.*`-Assembly. Dadurch besteht das Deployment nur aus EXE,
Konfiguration, Startskript und Dokumentation. Die lokal mit den
BizTalk-Verwaltungskomponenten installierte
`Microsoft.BizTalk.ExplorerOM.dll` wird zur Laufzeit aus GAC beziehungsweise
Installationspfad geladen. Die übrige BizTalk-Integration erfolgt über WMI und
`BTSTask.exe`.
## 3. Datenquellen ## 3. Datenquellen
@@ -85,7 +91,30 @@ Ermittelt werden unter anderem:
Kommandozeilenparameter überschreiben automatisch ermittelte Management-DB-Werte. Kommandozeilenparameter überschreiben automatisch ermittelte Management-DB-Werte.
### 3.2 BizTalk-WMI ### 3.2 BizTalk-Gruppe und ExplorerOM
Die Management-Datenbank wird zunächst aus Registry und lokalem
`MSBTS_GroupSetting` ermittelt. Kommandozeilenparameter haben Vorrang. Die
verwendete WQL-Abfrage wird mit Laufzeit und Datensatzanzahl protokolliert:
```sql
SELECT Name,MgmtDbServerName,MgmtDbName FROM MSBTS_GroupSetting
```
Die vollständige Anwendungsliste wird anschließend über
`Microsoft.BizTalk.ExplorerOM.BtsCatalogExplorer` und integrierte
Windows-Authentifizierung aus der BizTalk Management Database gelesen. Es werden
keine ändernden ExplorerOM-Methoden aufgerufen. Die Connection String wird nicht
protokolliert.
Dieser Aufbau vermeidet die nicht verlässlich vorhandene WMI-Klasse
`MSBTS_Application`, die je nach Installation mit `Invalid class` abbrechen kann.
ExplorerOM-Assemblyname, Version und Dateipfad werden protokolliert. Wenn
`MSBTS_GroupSetting` nicht gelesen werden kann, darf das automatisch oder per
`--management-server`/`--management-database` ermittelte Datenbankziel weiterhin
für ExplorerOM verwendet werden.
### 3.3 Optionale BizTalk-WMI-Daten
Primärer Namespace: Primärer Namespace:
@@ -93,11 +122,6 @@ Primärer Namespace:
root\MicrosoftBizTalkServer root\MicrosoftBizTalkServer
``` ```
Pflichtquelle:
- `MSBTS_Application`
- `MSBTS_GroupSetting` wird zusätzlich zur sicheren Erkennung von Management SQL Server und Datenbank verwendet
Optionale beziehungsweise versionsabhängige Artefaktklassen: Optionale beziehungsweise versionsabhängige Artefaktklassen:
- `MSBTS_Orchestration` - `MSBTS_Orchestration`
@@ -115,13 +139,16 @@ Optionale beziehungsweise versionsabhängige Artefaktklassen:
- `MSBTS_HostSetting` - `MSBTS_HostSetting`
- `MSBTS_HostInstance` - `MSBTS_HostInstance`
Eine fehlende optionale Klasse erzeugt einen Hinweis, verhindert aber nicht die Anwendungsliste oder den Word-Bericht. Eine fehlende optionale Klasse erzeugt einen Hinweis, verhindert aber nicht die
ExplorerOM-Anwendungsliste oder den Word-Bericht. Ist der gesamte BizTalk-WMI-
Namespace für diese Zusatzdaten nicht erreichbar, wird der WMI-Teil übersprungen;
die zuvor geladene Anwendungsliste bleibt erhalten.
Zum Schutz vor ungewöhnlich großen Artefaktmengen begrenzt `MaxArtifactDetailsPerType` Zum Schutz vor ungewöhnlich großen Artefaktmengen begrenzt `MaxArtifactDetailsPerType`
die aufgenommenen Detaildatensätze pro WMI-Klasse auf standardmäßig 5000. Die die aufgenommenen Detaildatensätze pro WMI-Klasse auf standardmäßig 5000. Die
Anwendungsliste selbst wird nicht begrenzt. Anwendungsliste selbst wird nicht begrenzt.
### 3.3 BTSTask-Gruppenbinding ### 3.4 BTSTask-Gruppenbinding
Das Tool verwendet: Das Tool verwendet:
@@ -144,7 +171,7 @@ Die temporäre XML-Datei wird:
Microsoft dokumentiert, dass Kennwörter beim Binding-Export entfernt werden. Zusätzlich redigiert das Tool sensitive Element-, Attribut- und URI-Werte. Microsoft dokumentiert, dass Kennwörter beim Binding-Export entfernt werden. Zusätzlich redigiert das Tool sensitive Element-, Attribut- und URI-Werte.
### 3.4 Offline-Binding ### 3.5 Offline-Binding
Für Diagnose- oder Berechtigungsfälle kann ein vorhandener Export verarbeitet werden: Für Diagnose- oder Berechtigungsfälle kann ein vorhandener Export verarbeitet werden:
@@ -306,8 +333,14 @@ Das DR-Runbook muss mindestens enthalten:
## 10. Resilienz ## 10. Resilienz
- Collector-Isolation mit eigenem Status und Zeitmessung - Collector-Isolation mit eigenem Status und Zeitmessung
- WMI-Timeout - WMI nur lokal über `\\.\root\MicrosoftBizTalkServer`
- WMI-Timeout, WQL-, Laufzeit- und Datensatzprotokollierung
- vollständige Anwendungsliste über read-only ExplorerOM statt
`MSBTS_Application`
- dynamische ExplorerOM-Auflösung aus GAC und BizTalk-Installationspfad
- WMI-Zusatzdaten dürfen nach erfolgreicher ExplorerOM-Erfassung ausfallen
- Prozess-Timeout für `BTSTask` - Prozess-Timeout für `BTSTask`
- live gespiegelte, secret-bereinigte BTSTask-Ausgabe
- WMI-Fallback für SAP-Endpunkte - WMI-Fallback für SAP-Endpunkte
- Offline-Bindingmodus - Offline-Bindingmodus
- optionale WMI-Klassen stoppen den Lauf nicht - optionale WMI-Klassen stoppen den Lauf nicht
@@ -317,6 +350,8 @@ Das DR-Runbook muss mindestens enthalten:
- atomare DOCX-Ausgabe - atomare DOCX-Ausgabe
- eindeutige Dateinamen mit Umgebung, Server und Zeitstempel - eindeutige Dateinamen mit Umgebung, Server und Zeitstempel
- Exitcode `1` bei unvollständigem Pflichtabschnitt - Exitcode `1` bei unvollständigem Pflichtabschnitt
- vollständige Ausnahmeketten mit Typ, HRESULT, WMI-Status, inneren Ausnahmen
und Stacktrace auf Konsole und im Log
## 11. DOCX-Erzeugung ohne Office ## 11. DOCX-Erzeugung ohne Office
@@ -343,10 +378,12 @@ Auf jedem der beiden BizTalk Server:
3. `BizTalkSapEnvironmentInventory.exe --self-test` ausführen. 3. `BizTalkSapEnvironmentInventory.exe --self-test` ausführen.
4. Erfassung starten. 4. Erfassung starten.
5. Exitcode prüfen. 5. Exitcode prüfen.
6. DOCX und Log gemeinsam kontrollieren. 6. Im Log `MSBTS_GroupSetting`, Datensatzanzahl und Management-Datenbankziel prüfen.
7. Anwendungsliste gegen BizTalk Administration abgleichen. 7. ExplorerOM-Assemblyname, Version und Pfad kontrollieren.
8. SAP-Endpunkte und Hostzuordnung prüfen. 8. DOCX und Log gemeinsam kontrollieren.
9. WE20-/WE21-/Credential-/DR-Findings an SAP Basis geben. 9. Anwendungsliste gegen BizTalk Administration abgleichen.
10. SAP-Endpunkte und Hostzuordnung prüfen.
11. WE20-/WE21-/Credential-/DR-Findings an SAP Basis geben.
ACC: ACC:
@@ -382,6 +419,7 @@ Tests prüfen:
- Parsing eines WCF-SAP-Gruppenbindings - Parsing eines WCF-SAP-Gruppenbindings
- SAP-Client, Host und `UseSnc` - SAP-Client, Host und `UseSnc`
- Anwendungszuordnung - Anwendungszuordnung
- Diagnose-Logging mit HRESULT und innerer Reflection-Ausnahme
- DOCX-Paketstruktur - DOCX-Paketstruktur
- XML-Gültigkeit aller DOCX-Parts - XML-Gültigkeit aller DOCX-Parts
- Ausschluss von Testkennwörtern - Ausschluss von Testkennwörtern
@@ -395,12 +433,15 @@ Tests prüfen:
- keine Garantie, dass Schemanamen alle verwendeten IDoc-Erweiterungen enthalten - keine Garantie, dass Schemanamen alle verwendeten IDoc-Erweiterungen enthalten
- dynamisch zur Laufzeit konstruierte SAP-Adressen können im statischen Binding fehlen - dynamisch zur Laufzeit konstruierte SAP-Adressen können im statischen Binding fehlen
- WMI-Klassen unterscheiden sich teilweise je BizTalk-Version/CU - WMI-Klassen unterscheiden sich teilweise je BizTalk-Version/CU
- die lokal installierte ExplorerOM-Version muss zur lokalen BizTalk-Installation
passen
- lokaler Ist-Stand; ACC und PROD müssen getrennt ausgeführt werden - lokaler Ist-Stand; ACC und PROD müssen getrennt ausgeführt werden
## 15. Referenzen ## 15. Referenzen
- Microsoft: BizTalk Server 2020 Hardware and Software Requirements - Microsoft: BizTalk Server 2020 Hardware and Software Requirements
- Microsoft: `BTSTask ExportBindings` - Microsoft: `BTSTask ExportBindings`
- Microsoft: BizTalk Explorer Object Model
- Microsoft: BizTalk WMI Technical Reference - Microsoft: BizTalk WMI Technical Reference
- Microsoft: WCF-SAP Port Configuration - Microsoft: WCF-SAP Port Configuration
- Microsoft: SAP System Connection URI - Microsoft: SAP System Connection URI
+47 -11
View File
@@ -7,14 +7,18 @@ Das Tool wird einmal auf dem BizTalk Server in `ACC` und einmal auf dem BizTalk
- einem BizTalk Server 2020 - einem BizTalk Server 2020
- einem separaten SQL Server für die BizTalk-Datenbanken - einem separaten SQL Server für die BizTalk-Datenbanken
Microsoft Office, Word, PowerShell, ein .NET SDK, Internetzugriff und zusätzliche BizTalk-Programmier-DLLs werden auf dem Zielserver nicht benötigt. Microsoft Office, Word, PowerShell, ein .NET SDK, Internetzugriff und separat
mitgelieferte BizTalk-DLLs werden auf dem Zielserver nicht benötigt. Die mit den
lokalen BizTalk-Verwaltungskomponenten installierte
`Microsoft.BizTalk.ExplorerOM.dll` wird zur Laufzeit aus GAC beziehungsweise
BizTalk-Installationspfad geladen.
## Erfasster Umfang ## Erfasster Umfang
### Vollständige BizTalk-Anwendungsliste ### Vollständige BizTalk-Anwendungsliste
- alle Anwendungen aus `MSBTS_Application` - alle Anwendungen read-only aus dem BizTalk Explorer Object Model
- Anwendungsstatus und Beschreibung - Anwendungsstatus und Beschreibung, soweit ExplorerOM sie bereitstellt
- Anzahl und Zuordnung von Orchestrierungen - Anzahl und Zuordnung von Orchestrierungen
- Send Ports und Send Port Groups - Send Ports und Send Port Groups
- Receive Ports und Receive Locations - Receive Ports und Receive Locations
@@ -69,7 +73,7 @@ Das Tool erfindet hierfür keine Werte. Es dokumentiert nur lokal belegbare Indi
## Sicherheitsprinzip ## Sicherheitsprinzip
- ausschließlich read-only WMI- und Registry-Abfragen - ausschließlich read-only ExplorerOM-, WMI- und Registry-Zugriffe
- `BTSTask ExportBindings /GroupLevel` wird nur zum Lesen verwendet - `BTSTask ExportBindings /GroupLevel` wird nur zum Lesen verwendet
- temporärer Binding-Export wird nach dem Parsen gelöscht - temporärer Binding-Export wird nach dem Parsen gelöscht
- BizTalk entfernt Kennwörter bereits beim Binding-Export - BizTalk entfernt Kennwörter bereits beim Binding-Export
@@ -86,6 +90,8 @@ DOCX und Log enthalten trotzdem interne Host-, Anwendungs-, Zertifikats- und Pfa
- Konto ist Mitglied von `BizTalk Server Administrators` oder besitzt vergleichbare Leserechte - Konto ist Mitglied von `BizTalk Server Administrators` oder besitzt vergleichbare Leserechte
- .NET Framework 4.7.2 oder höher - .NET Framework 4.7.2 oder höher
- Zugriff auf die konfigurierte BizTalk Management Database - Zugriff auf die konfigurierte BizTalk Management Database
- lokale BizTalk-Verwaltungskomponenten einschließlich
`Microsoft.BizTalk.ExplorerOM.dll`
- `BTSTask.exe` aus der lokalen BizTalk-Installation - `BTSTask.exe` aus der lokalen BizTalk-Installation
## Schnellstart ## Schnellstart
@@ -127,15 +133,25 @@ BizTalkSapEnvironmentInventory.exe ^
Das Tool zeigt jeden Abschnitt und die erzeugten Dateien direkt auf der Konsole an: Das Tool zeigt jeden Abschnitt und die erzeugten Dateien direkt auf der Konsole an:
```text ```text
[INFO] Starte Abschnitt: BizTalk-Anwendungen und WMI-Artefakte [INFO] Ermittle die BizTalk-Gruppe über MSBTS_GroupSetting.
[INFO] 34 BizTalk-Anwendung(en) über WMI gefunden. [DETAIL] WQL: SELECT Name,MgmtDbServerName,MgmtDbName FROM MSBTS_GroupSetting
[INFO] MSBTS_GroupSetting abgeschlossen: Datensätze=1; Dauer=84 ms.
[INFO] Suche die lokale Assembly Microsoft.BizTalk.ExplorerOM.dll.
[INFO] ExplorerOM-Assembly geladen: Microsoft.BizTalk.ExplorerOM, Version=...
[INFO] 34 BizTalk-Anwendung(en) geladen (412 ms).
[INFO] Verbinde lokal mit dem BizTalk-WMI-Namespace für optionale Zusatzdaten.
[INFO] WMI-Abfrage abgeschlossen: SendPort; Datensätze=91; Dauer=173 ms.
[INFO] Exportiere BizTalk-Gruppenbindings read-only mit BTSTask. [INFO] Exportiere BizTalk-Gruppenbindings read-only mit BTSTask.
[INFO] BTSTask ExportBindings erfolgreich abgeschlossen (1865 ms).
[INFO] 12 SAP-Endpunkt(e) aus dem Binding-Export ausgewertet. [INFO] 12 SAP-Endpunkt(e) aus dem Binding-Export ausgewertet.
[INFO] Erzeuge Microsoft-Word-Dokument: C:\BizTalk-Doku\ACC\... [INFO] Ergebnisübersicht: Anwendungen=34; SAP-Endpunkte=12; Findings=4.
[INFO] Word-Dokument erfolgreich erzeugt: C:\BizTalk-Doku\ACC\...
``` ```
Fehler eines Collectors stoppen die übrigen Abschnitte nicht. Sie erscheinen im Log, im Erfassungsstatus und als Finding im Word-Bericht. Konsole und Log erhalten dieselben Fortschritts- und Diagnosemeldungen. Fehler
enthalten Ausnahmetyp, HRESULT, WMI-Status, innere Ausnahmen und Stacktrace.
Fehler eines Collectors stoppen die übrigen Abschnitte nicht. Fehlende optionale
WMI-Klassen verwerfen insbesondere nicht die über ExplorerOM geladene
Anwendungsliste.
## Ausgabedateien ## Ausgabedateien
@@ -200,7 +216,28 @@ Die Gitea-Workflowdatei `.gitea/workflows/build.yml` baut, testet und veröffent
- lokal auf dem BizTalk Server ausführen - lokal auf dem BizTalk Server ausführen
- Konto und Mitgliedschaft in den BizTalk-Administrator-/Operator-Gruppen prüfen - Konto und Mitgliedschaft in den BizTalk-Administrator-/Operator-Gruppen prüfen
- WMI-Namespace `root\MicrosoftBizTalkServer` prüfen - im Log das ermittelte Management-Datenbankziel prüfen
- geladene ExplorerOM-Assembly einschließlich Version und Pfad prüfen
- Zugriff auf die BizTalk Management Database mit integrierter
Windows-Authentifizierung prüfen
- bei fehlender Erkennung `--management-server` und
`--management-database` explizit angeben
### WMI meldet `Invalid class`
Die Anwendungsliste verwendet nicht mehr die nicht verlässlich vorhandene Klasse
`MSBTS_Application`, sondern ExplorerOM. Ein `Invalid class` für eine optionale
Artefaktklasse erscheint mit WQL, HRESULT und WMI-Status im Log, wird als Finding
übernommen und der Lauf wird fortgesetzt. Schlägt bereits
`MSBTS_GroupSetting` fehl, kann die Management-Datenbank über die beiden
`--management-*`-Optionen vorgegeben werden.
### ExplorerOM-Assembly fehlt
- prüfen, ob die BizTalk-Verwaltungskomponenten lokal installiert sind
- im Log die geprüften GAC-/Installationspfade kontrollieren
- keine DLL von einem anderen BizTalk-Server oder einer anderen CU-Version
in den Deployment-Ordner kopieren
### BTSTask findet die Management Database nicht ### BTSTask findet die Management Database nicht
@@ -228,4 +265,3 @@ BizTalkSapEnvironmentInventory.exe ^
- DOCX-Findings und Log prüfen - DOCX-Findings und Log prüfen
Weitere Architektur- und Sicherheitsdetails stehen in [Dokumentation.md](Dokumentation.md). Weitere Architektur- und Sicherheitsdetails stehen in [Dokumentation.md](Dokumentation.md).
@@ -530,7 +530,13 @@ namespace BizTalkSapEnvironmentInventory.Collectors
} }
logger.Info("Exportiere BizTalk-Gruppenbindings read-only mit BTSTask."); logger.Info("Exportiere BizTalk-Gruppenbindings read-only mit BTSTask.");
logger.Detail("BTSTask-Pfad: " + btsTaskPath);
logger.Detail("BTSTask-Timeout: "
+ processTimeout.TotalSeconds.ToString(CultureInfo.InvariantCulture)
+ " Sekunden.");
var output = new StringBuilder(); var output = new StringBuilder();
var outputSync = new object();
var timer = Stopwatch.StartNew();
var startInfo = new ProcessStartInfo var startInfo = new ProcessStartInfo
{ {
FileName = btsTaskPath, FileName = btsTaskPath,
@@ -548,14 +554,24 @@ namespace BizTalkSapEnvironmentInventory.Collectors
{ {
if (!string.IsNullOrWhiteSpace(args.Data)) if (!string.IsNullOrWhiteSpace(args.Data))
{ {
output.AppendLine(args.Data); var safeLine = SensitiveDataSanitizer.SanitizeText(args.Data);
lock (outputSync)
{
output.AppendLine(safeLine);
}
logger.Detail("BTSTask: " + safeLine);
} }
}; };
process.ErrorDataReceived += (sender, args) => process.ErrorDataReceived += (sender, args) =>
{ {
if (!string.IsNullOrWhiteSpace(args.Data)) if (!string.IsNullOrWhiteSpace(args.Data))
{ {
output.AppendLine(args.Data); var safeLine = SensitiveDataSanitizer.SanitizeText(args.Data);
lock (outputSync)
{
output.AppendLine(safeLine);
}
logger.Detail("BTSTask: " + safeLine);
} }
}; };
@@ -575,20 +591,32 @@ namespace BizTalkSapEnvironmentInventory.Collectors
"BTSTask ExportBindings überschritt " + processTimeout.TotalSeconds + " Sekunden."); "BTSTask ExportBindings überschritt " + processTimeout.TotalSeconds + " Sekunden.");
} }
process.WaitForExit(); process.WaitForExit();
timer.Stop();
if (process.ExitCode != 0) if (process.ExitCode != 0)
{ {
string errorOutput;
lock (outputSync)
{
errorOutput = output.ToString();
}
throw new InvalidOperationException( throw new InvalidOperationException(
"BTSTask ExportBindings meldete Exitcode " "BTSTask ExportBindings meldete Exitcode "
+ process.ExitCode + process.ExitCode
+ ": " + ": "
+ LastLines(output.ToString(), 8)); + LastLines(errorOutput, 8));
} }
logger.Info("BTSTask ExportBindings erfolgreich abgeschlossen ("
+ timer.ElapsedMilliseconds + " ms).");
} }
if (!File.Exists(destination) || new FileInfo(destination).Length == 0) if (!File.Exists(destination) || new FileInfo(destination).Length == 0)
{ {
throw new InvalidDataException("BTSTask erzeugte keine Binding-Datei."); throw new InvalidDataException("BTSTask erzeugte keine Binding-Datei.");
} }
logger.Detail("Temporärer Binding-Export: "
+ new FileInfo(destination).Length
.ToString(CultureInfo.InvariantCulture)
+ " Bytes.");
} }
private string ResolveBtsTaskPath() private string ResolveBtsTaskPath()
@@ -1,19 +1,29 @@
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.IO;
using System.Linq; using System.Linq;
using System.Management; using System.Management;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.Principal;
using BizTalkSapEnvironmentInventory.Infrastructure; using BizTalkSapEnvironmentInventory.Infrastructure;
using BizTalkSapEnvironmentInventory.Models; using BizTalkSapEnvironmentInventory.Models;
namespace BizTalkSapEnvironmentInventory.Collectors namespace BizTalkSapEnvironmentInventory.Collectors
{ {
/// <summary> /// <summary>
/// Liest Anwendungen, Artefakte, Adapter und Hosts aus dem BizTalk-WMI-Provider. /// Liest die vollständige Anwendungsliste über ExplorerOM und ergänzt sie um
/// optionale Artefakt-, Adapter- und Hostdaten aus dem BizTalk-WMI-Provider.
/// </summary> /// </summary>
internal sealed class BizTalkWmiCollector internal sealed class BizTalkWmiCollector
{ {
private const string NamespacePath = @"root\MicrosoftBizTalkServer"; private const string WmiNamespace = @"\\.\root\MicrosoftBizTalkServer";
private const string ExplorerAssemblyName = "Microsoft.BizTalk.ExplorerOM";
private const string ExplorerTypeName =
"Microsoft.BizTalk.ExplorerOM.BtsCatalogExplorer";
private readonly InventoryDocument document; private readonly InventoryDocument document;
private readonly ConsoleFileLogger logger; private readonly ConsoleFileLogger logger;
@@ -31,18 +41,104 @@ namespace BizTalkSapEnvironmentInventory.Collectors
this.logger = logger; this.logger = logger;
timeout = TimeSpan.FromSeconds(Math.Max(5, timeoutSeconds)); timeout = TimeSpan.FromSeconds(Math.Max(5, timeoutSeconds));
this.maxArtifactDetailsPerType = Math.Max(100, maxArtifactDetailsPerType); this.maxArtifactDetailsPerType = Math.Max(100, maxArtifactDetailsPerType);
scope = new ManagementScope(@"\\" + Environment.MachineName + @"\" + NamespacePath); scope = new ManagementScope(WmiNamespace);
scope.Options.Timeout = timeout; scope.Options.Timeout = timeout;
scope.Options.EnablePrivileges = false;
scope.Options.Impersonation = ImpersonationLevel.Impersonate;
} }
/// <summary> /// <summary>
/// Baut die vollständige Anwendungsliste auf und ergänzt verfügbare Artefaktbeziehungen. /// Baut die vollständige Anwendungsliste auf und ergänzt verfügbare WMI-Daten.
/// </summary> /// </summary>
public void Collect() public void Collect()
{ {
scope.Connect(); LogRuntimeInformation();
CollectApplications(); CollectApplications();
if (TryConnectWmi())
{
CollectOptionalWmiData();
}
document.Applications.Sort((left, right) =>
string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase));
}
private void CollectApplications()
{
if (string.IsNullOrWhiteSpace(document.System.ManagementServer))
{
throw new InvalidOperationException(
"Der BizTalk Management SQL Server wurde weder über "
+ "MSBTS_GroupSetting/Registry noch über --management-server ermittelt.");
}
if (string.IsNullOrWhiteSpace(document.System.ManagementDatabase))
{
throw new InvalidOperationException(
"Die BizTalk Management Database wurde weder automatisch noch "
+ "über --management-database ermittelt.");
}
var explorerType = ResolveExplorerType();
object explorer = null;
try
{
explorer = Activator.CreateInstance(explorerType);
logger.Info(
"Öffne das BizTalk Explorer Object Model ausschließlich lesend.");
logger.Info("ExplorerOM-Ziel: Server="
+ document.System.ManagementServer
+ "; Datenbank="
+ document.System.ManagementDatabase
+ ".");
// Die integrierte Verbindungszeichenfolge wird bewusst nicht protokolliert.
SetRequiredProperty(
explorer,
"ConnectionString",
"Data Source=" + document.System.ManagementServer
+ ";Initial Catalog=" + document.System.ManagementDatabase
+ ";Integrated Security=SSPI;Persist Security Info=False;");
var timer = Stopwatch.StartNew();
var applications = EnumerateRequiredProperty(
explorer,
"Applications",
"BizTalk-Anwendungen");
timer.Stop();
logger.Info(applications.Count + " BizTalk-Anwendung(en) geladen ("
+ timer.ElapsedMilliseconds + " ms).");
foreach (var item in applications)
{
var name = RequiredText(item, "Name");
var application = GetOrCreateApplication(name);
application.Description = OptionalText(item, "Description");
application.Status = FormatStatus(OptionalText(item, "Status"));
logger.Detail("ExplorerOM-Anwendung: " + name
+ "; Status=" + application.Status + ".");
}
if (document.Applications.Count == 0)
{
throw new InvalidOperationException(
"Das BizTalk Explorer Object Model lieferte keine Anwendungen.");
}
}
finally
{
var disposable = explorer as IDisposable;
if (disposable != null)
{
disposable.Dispose();
logger.Detail("BizTalk Explorer Object Model wurde geschlossen.");
}
}
}
private void CollectOptionalWmiData()
{
var artifactClasses = new[] var artifactClasses = new[]
{ {
new ClassDescriptor("MSBTS_Orchestration", "Orchestration"), new ClassDescriptor("MSBTS_Orchestration", "Orchestration"),
@@ -72,41 +168,210 @@ namespace BizTalkSapEnvironmentInventory.Collectors
TryCollectWmiSapEndpoints("MSBTS_SendPort", "Senden"); TryCollectWmiSapEndpoints("MSBTS_SendPort", "Senden");
TryCollectWmiSapEndpoints("MSBTS_ReceiveLocation", "Empfangen"); TryCollectWmiSapEndpoints("MSBTS_ReceiveLocation", "Empfangen");
document.Applications.Sort((left, right) =>
string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase));
} }
private void CollectApplications() private void LogRuntimeInformation()
{ {
var rows = Query("SELECT * FROM MSBTS_Application"); logger.Info("Diagnose: Computer=" + Environment.MachineName
foreach (var row in rows) + "; Betriebssystem=" + Environment.OSVersion.VersionString
+ "; CLR=" + Environment.Version
+ "; Prozess=" + (Environment.Is64BitProcess ? "64-Bit" : "32-Bit")
+ "; Betriebssystem="
+ (Environment.Is64BitOperatingSystem ? "64-Bit" : "32-Bit")
+ ".");
logger.Detail("WMI-Namespace: " + WmiNamespace);
logger.Detail("WMI-Timeout: "
+ timeout.TotalSeconds.ToString(CultureInfo.InvariantCulture)
+ " Sekunden.");
try
{ {
var name = First(row, "Name", "ApplicationName"); using (var identity = WindowsIdentity.GetCurrent())
if (string.IsNullOrWhiteSpace(name)) {
var principal = new WindowsPrincipal(identity);
logger.Info("Sicherheitskontext: Benutzer=" + identity.Name
+ "; administrativ="
+ (principal.IsInRole(WindowsBuiltInRole.Administrator)
? "Ja"
: "Nein")
+ ".");
}
}
catch (Exception exception)
{
// Diagnosefehler dürfen die eigentliche Inventarisierung nicht verhindern.
logger.Warning("Sicherheitskontext konnte nicht ermittelt werden: "
+ exception.Message);
}
}
private bool TryConnectWmi()
{
var timer = Stopwatch.StartNew();
logger.Info(
"Verbinde lokal mit dem BizTalk-WMI-Namespace für optionale Zusatzdaten.");
try
{
scope.Connect();
timer.Stop();
logger.Info("BizTalk-WMI-Verbindung hergestellt ("
+ timer.ElapsedMilliseconds + " ms).");
return true;
}
catch (Exception exception) when (IsRecoverableWmiException(exception))
{
timer.Stop();
document.Findings.Add(new Finding
{
Severity = "Warnung",
Area = "BizTalk WMI",
Message =
"Der BizTalk-WMI-Namespace ist für optionale Artefakt-, "
+ "Adapter- und Hostdaten nicht erreichbar: " + exception.Message,
RecommendedAction =
"WMI-Provider und Berechtigungen anhand des Logs prüfen. "
+ "Die über ExplorerOM geladene Anwendungsliste bleibt verwendbar.",
Owner = "BizTalk-Betrieb"
});
logger.Exception("BizTalk-WMI-Verbindung fehlgeschlagen", exception);
logger.Warning(
"Optionale WMI-Zusatzdaten werden übersprungen; "
+ "die ExplorerOM-Anwendungsliste bleibt erhalten.");
return false;
}
}
private Type ResolveExplorerType()
{
logger.Info("Suche die lokale Assembly " + ExplorerAssemblyName + ".dll.");
var qualifiedTypeName = ExplorerTypeName + ", " + ExplorerAssemblyName;
var explorerType = Type.GetType(qualifiedTypeName, false);
if (explorerType != null)
{
LogExplorerAssembly(explorerType.Assembly);
return explorerType;
}
Assembly assembly = null;
try
{
assembly = Assembly.Load(ExplorerAssemblyName);
}
catch (Exception exception)
{
logger.Detail("Assembly.Load fehlgeschlagen: "
+ exception.GetType().Name + ": " + exception.Message);
}
if (assembly == null)
{
#pragma warning disable 618
// Kompatibilitäts-Fallback für unterschiedliche BizTalk-GAC-Versionen.
assembly = Assembly.LoadWithPartialName(ExplorerAssemblyName);
#pragma warning restore 618
}
if (assembly == null)
{
assembly = LoadExplorerAssemblyFromInstallDirectory();
}
if (assembly == null)
{
throw new FileNotFoundException(
"Microsoft.BizTalk.ExplorerOM.dll wurde weder im GAC noch "
+ "im BizTalk-Installationsverzeichnis gefunden. "
+ "Die BizTalk-Verwaltungskomponenten müssen lokal installiert sein.");
}
explorerType = assembly.GetType(ExplorerTypeName, false);
if (explorerType == null)
{
throw new TypeLoadException(
"Typ " + ExplorerTypeName + " fehlt in " + assembly.FullName + ".");
}
LogExplorerAssembly(assembly);
return explorerType;
}
private Assembly LoadExplorerAssemblyFromInstallDirectory()
{
var candidates = new List<string>();
if (!string.IsNullOrWhiteSpace(document.System.BizTalkInstallPath))
{
candidates.Add(Path.Combine(
document.System.BizTalkInstallPath,
ExplorerAssemblyName + ".dll"));
}
var roots = new[]
{
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86),
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles)
};
var folders = new[]
{
"Microsoft BizTalk Server",
"Microsoft BizTalk Server 2020"
};
foreach (var root in roots.Where(item => !string.IsNullOrWhiteSpace(item)))
{
foreach (var folder in folders)
{
candidates.Add(Path.Combine(
root,
folder,
ExplorerAssemblyName + ".dll"));
}
}
foreach (var path in candidates.Distinct(StringComparer.OrdinalIgnoreCase))
{
logger.Detail("Prüfe ExplorerOM-Assemblypfad: " + path);
if (!File.Exists(path))
{ {
continue; continue;
} }
GetOrCreateApplication(name).Description = First(row, "Description"); try
GetOrCreateApplication(name).Status = FormatStatus(First(row, "Status"));
}
if (document.Applications.Count == 0)
{ {
throw new InvalidOperationException( return Assembly.LoadFrom(path);
"WMI lieferte keine BizTalk-Anwendungen. Berechtigungen und BizTalk-Gruppe prüfen."); }
catch (Exception exception)
{
logger.Detail("Assembly.LoadFrom fehlgeschlagen: "
+ exception.GetType().Name + ": " + exception.Message);
}
} }
logger.Info(document.Applications.Count + " BizTalk-Anwendung(en) über WMI gefunden."); return null;
}
private void LogExplorerAssembly(Assembly assembly)
{
logger.Info("ExplorerOM-Assembly geladen: " + assembly.FullName + ".");
try
{
logger.Detail("ExplorerOM-Assemblypfad: " + assembly.Location);
}
catch (NotSupportedException)
{
logger.Detail(
"ExplorerOM-Assemblypfad ist für diese Assembly nicht verfügbar.");
}
} }
private void TryCollectArtifacts(ClassDescriptor descriptor) private void TryCollectArtifacts(ClassDescriptor descriptor)
{ {
List<ManagementObject> rows = null;
try try
{ {
var count = 0; var count = 0;
foreach (var row in Query("SELECT * FROM " + descriptor.ClassName)) rows = Query(
descriptor.DisplayName,
"SELECT * FROM " + descriptor.ClassName);
foreach (var row in rows)
{ {
if (count >= maxArtifactDetailsPerType) if (count >= maxArtifactDetailsPerType)
{ {
@@ -164,10 +429,14 @@ namespace BizTalkSapEnvironmentInventory.Collectors
logger.Info(descriptor.DisplayName + ": " + count + " Artefakt(e)."); logger.Info(descriptor.DisplayName + ": " + count + " Artefakt(e).");
} }
catch (ManagementException exception) catch (Exception exception) when (IsRecoverableWmiException(exception))
{ {
AddOptionalWmiFinding(descriptor.ClassName, exception); AddOptionalWmiFinding(descriptor.ClassName, exception);
} }
finally
{
DisposeRows(rows);
}
} }
private bool TryCollectComponents( private bool TryCollectComponents(
@@ -176,9 +445,11 @@ namespace BizTalkSapEnvironmentInventory.Collectors
List<ComponentRecord> target, List<ComponentRecord> target,
bool onlySap) bool onlySap)
{ {
List<ManagementObject> rows = null;
try try
{ {
foreach (var row in Query("SELECT * FROM " + className)) rows = Query(category, "SELECT * FROM " + className);
foreach (var row in rows)
{ {
if (onlySap && !LooksLikeSap(row)) if (onlySap && !LooksLikeSap(row))
{ {
@@ -199,18 +470,26 @@ namespace BizTalkSapEnvironmentInventory.Collectors
} }
return true; return true;
} }
catch (ManagementException exception) catch (Exception exception) when (IsRecoverableWmiException(exception))
{ {
AddOptionalWmiFinding(className, exception); AddOptionalWmiFinding(className, exception);
return false; return false;
} }
finally
{
DisposeRows(rows);
}
} }
private void TryCollectWmiSapEndpoints(string className, string direction) private void TryCollectWmiSapEndpoints(string className, string direction)
{ {
List<ManagementObject> rows = null;
try try
{ {
foreach (var row in Query("SELECT * FROM " + className)) rows = Query(
className + " SAP-Fallback",
"SELECT * FROM " + className);
foreach (var row in rows)
{ {
if (!LooksLikeSap(row)) if (!LooksLikeSap(row))
{ {
@@ -249,13 +528,17 @@ namespace BizTalkSapEnvironmentInventory.Collectors
document.SapEndpoints.Add(endpoint); document.SapEndpoints.Add(endpoint);
} }
} }
catch (ManagementException exception) catch (Exception exception) when (IsRecoverableWmiException(exception))
{ {
AddOptionalWmiFinding(className + " SAP-Fallback", exception); AddOptionalWmiFinding(className + " SAP-Fallback", exception);
} }
finally
{
DisposeRows(rows);
}
} }
private List<ManagementObject> Query(string query) private List<ManagementObject> Query(string name, string query)
{ {
var options = new EnumerationOptions var options = new EnumerationOptions
{ {
@@ -263,14 +546,124 @@ namespace BizTalkSapEnvironmentInventory.Collectors
Rewindable = false, Rewindable = false,
Timeout = timeout Timeout = timeout
}; };
var timer = Stopwatch.StartNew();
logger.Info("WMI-Abfrage startet: " + name + ".");
logger.Detail("WQL: " + query);
using (var searcher = new ManagementObjectSearcher( using (var searcher = new ManagementObjectSearcher(
scope, scope,
new ObjectQuery(query), new ObjectQuery(query),
options)) options))
{ {
return searcher.Get().Cast<ManagementObject>().ToList(); using (var rows = searcher.Get())
{
var result = rows.Cast<ManagementObject>().ToList();
timer.Stop();
logger.Info("WMI-Abfrage abgeschlossen: " + name
+ "; Datensätze=" + result.Count
+ "; Dauer=" + timer.ElapsedMilliseconds + " ms.");
return result;
} }
} }
}
private static IList<object> EnumerateRequiredProperty(
object instance,
string propertyName,
string description)
{
var value = RequiredProperty(instance, propertyName);
var enumerable = value as IEnumerable;
if (enumerable == null)
{
throw new InvalidOperationException(
description + " ist keine aufzählbare Sammlung.");
}
var result = new List<object>();
foreach (var item in enumerable)
{
if (item != null)
{
result.Add(item);
}
}
return result;
}
private static string RequiredText(object instance, string propertyName)
{
var value = Convert.ToString(
RequiredProperty(instance, propertyName),
CultureInfo.InvariantCulture);
if (string.IsNullOrWhiteSpace(value))
{
throw new InvalidOperationException(
instance.GetType().FullName + "." + propertyName + " ist leer.");
}
return value.Trim();
}
private string OptionalText(object instance, string propertyName)
{
var property = instance.GetType().GetProperty(
propertyName,
BindingFlags.Instance | BindingFlags.Public);
if (property == null)
{
return string.Empty;
}
try
{
return Convert.ToString(
property.GetValue(instance, null),
CultureInfo.InvariantCulture) ?? string.Empty;
}
catch (Exception exception) when (
exception is TargetInvocationException
|| exception is COMException
|| exception is NotSupportedException)
{
logger.Detail("Optionale ExplorerOM-Eigenschaft "
+ instance.GetType().FullName
+ "."
+ propertyName
+ " konnte nicht gelesen werden: "
+ exception.Message);
return string.Empty;
}
}
private static object RequiredProperty(object instance, string propertyName)
{
var property = instance.GetType().GetProperty(
propertyName,
BindingFlags.Instance | BindingFlags.Public);
if (property == null)
{
throw new MissingMemberException(instance.GetType().FullName, propertyName);
}
return property.GetValue(instance, null);
}
private static void SetRequiredProperty(
object instance,
string propertyName,
object value)
{
var property = instance.GetType().GetProperty(
propertyName,
BindingFlags.Instance | BindingFlags.Public);
if (property == null || !property.CanWrite)
{
throw new MissingMemberException(instance.GetType().FullName, propertyName);
}
property.SetValue(instance, value, null);
}
private ApplicationRecord GetOrCreateApplication(string name) private ApplicationRecord GetOrCreateApplication(string name)
{ {
@@ -405,7 +798,7 @@ namespace BizTalkSapEnvironmentInventory.Collectors
} }
} }
private void AddOptionalWmiFinding(string className, ManagementException exception) private void AddOptionalWmiFinding(string className, Exception exception)
{ {
document.Findings.Add(new Finding document.Findings.Add(new Finding
{ {
@@ -415,7 +808,32 @@ namespace BizTalkSapEnvironmentInventory.Collectors
RecommendedAction = "Nur relevant, wenn die zugehörige Artefaktspalte vollständig benötigt wird.", RecommendedAction = "Nur relevant, wenn die zugehörige Artefaktspalte vollständig benötigt wird.",
Owner = "BizTalk-Betrieb" Owner = "BizTalk-Betrieb"
}); });
logger.Warning("Optionale WMI-Abfrage " + className + " fehlgeschlagen: " + exception.Message); logger.Exception(
"Optionale WMI-Abfrage " + className + " fehlgeschlagen",
exception);
}
private static bool IsRecoverableWmiException(Exception exception)
{
return exception is ManagementException
|| exception is COMException
|| exception is UnauthorizedAccessException;
}
private static void DisposeRows(IEnumerable<ManagementObject> rows)
{
if (rows == null)
{
return;
}
foreach (var row in rows)
{
if (row != null)
{
row.Dispose();
}
}
} }
private sealed class ClassDescriptor private sealed class ClassDescriptor
@@ -46,15 +46,32 @@ namespace BizTalkSapEnvironmentInventory.Collectors
/// </summary> /// </summary>
public void Collect() public void Collect()
{ {
logger.Info("Prüfe installierte SAP-/BizTalk-Adapter-Komponenten.");
CollectInstalledProducts(); CollectInstalledProducts();
logger.Info("Lese konfigurierte SNC- und SAP-Runtime-Pfade.");
var configuredPaths = CollectConfiguredSecurityPaths(); var configuredPaths = CollectConfiguredSecurityPaths();
logger.Info("Lese SAP-relevante Umgebungsvariablen.");
CollectEnvironment(document.RuntimeComponents); CollectEnvironment(document.RuntimeComponents);
logger.Info("Suche SAP-Runtime-Dateien in begrenzten Installationspfaden.");
CollectRuntimeFiles(configuredPaths); CollectRuntimeFiles(configuredPaths);
logger.Info("Prüfe sichere Metadaten lokaler PSE-/Security-Dateien.");
CollectSecurityFiles(configuredPaths); CollectSecurityFiles(configuredPaths);
if (includeCertificates) if (includeCertificates)
{ {
logger.Info("Lese SAP-relevante Zertifikatsmetadaten.");
CollectCertificates(); CollectCertificates();
} }
else
{
logger.Detail(
"Zertifikatserfassung ist per Konfiguration deaktiviert.");
}
logger.Info("SAP-Runtime-Erfassung abgeschlossen: Komponenten="
+ document.RuntimeComponents.Count
+ "; Security-Materialien="
+ document.SecurityMaterials.Count
+ ".");
if (!document.RuntimeComponents.Any(item => if (!document.RuntimeComponents.Any(item =>
string.Equals( string.Equals(
@@ -1,9 +1,14 @@
using System; using System;
using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.IO;
using System.Management; using System.Management;
using System.Reflection; using System.Reflection;
using System.Runtime.InteropServices;
using System.Security;
using Microsoft.Win32; using Microsoft.Win32;
using BizTalkSapEnvironmentInventory.Configuration; using BizTalkSapEnvironmentInventory.Configuration;
using BizTalkSapEnvironmentInventory.Infrastructure;
using BizTalkSapEnvironmentInventory.Models; using BizTalkSapEnvironmentInventory.Models;
namespace BizTalkSapEnvironmentInventory.Collectors namespace BizTalkSapEnvironmentInventory.Collectors
@@ -17,10 +22,17 @@ namespace BizTalkSapEnvironmentInventory.Collectors
private const string AdministrationKey = BizTalkKey + @"\Administration"; private const string AdministrationKey = BizTalkKey + @"\Administration";
private readonly CommandLineOptions options; private readonly CommandLineOptions options;
private readonly ConsoleFileLogger logger;
private readonly TimeSpan timeout;
public SystemCollector(CommandLineOptions options) public SystemCollector(
CommandLineOptions options,
ConsoleFileLogger logger,
int timeoutSeconds)
{ {
this.options = options; this.options = options;
this.logger = logger;
timeout = TimeSpan.FromSeconds(Math.Max(5, timeoutSeconds));
} }
/// <summary> /// <summary>
@@ -43,8 +55,11 @@ namespace BizTalkSapEnvironmentInventory.Collectors
".NET Runtime", ".NET Runtime",
Environment.Version.ToString())); Environment.Version.ToString()));
logger.Info("Lese lokale Windows-Betriebssysteminformationen.");
CollectOperatingSystem(document); CollectOperatingSystem(document);
logger.Info("Lese lokale BizTalk-Installation aus der Registry.");
CollectBizTalkRegistry(document); CollectBizTalkRegistry(document);
logger.Info("Ermittle die BizTalk-Gruppe über MSBTS_GroupSetting.");
CollectBizTalkGroupSetting(document); CollectBizTalkGroupSetting(document);
if (!string.IsNullOrWhiteSpace(options.ManagementServer)) if (!string.IsNullOrWhiteSpace(options.ManagementServer))
@@ -67,6 +82,11 @@ namespace BizTalkSapEnvironmentInventory.Collectors
document.System.Properties.Add(new NameValueRecord( document.System.Properties.Add(new NameValueRecord(
"BizTalk Management Database", "BizTalk Management Database",
EmptyAsUnknown(document.System.ManagementDatabase))); EmptyAsUnknown(document.System.ManagementDatabase)));
logger.Info("Management-Datenbankziel: Server="
+ EmptyAsUnknown(document.System.ManagementServer)
+ "; Datenbank="
+ EmptyAsUnknown(document.System.ManagementDatabase)
+ ".");
if (!string.Equals(document.EnvironmentName, "ACC", StringComparison.OrdinalIgnoreCase) if (!string.Equals(document.EnvironmentName, "ACC", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(document.EnvironmentName, "PROD", StringComparison.OrdinalIgnoreCase)) && !string.Equals(document.EnvironmentName, "PROD", StringComparison.OrdinalIgnoreCase))
@@ -82,29 +102,79 @@ namespace BizTalkSapEnvironmentInventory.Collectors
} }
} }
private static void CollectOperatingSystem(InventoryDocument document) private void CollectOperatingSystem(InventoryDocument document)
{ {
using (var searcher = new ManagementObjectSearcher( const string query =
"root\\cimv2", "SELECT Caption,Version,BuildNumber,OSArchitecture,LastBootUpTime "
"SELECT Caption,Version,BuildNumber,OSArchitecture,LastBootUpTime FROM Win32_OperatingSystem")) + "FROM Win32_OperatingSystem";
var timer = Stopwatch.StartNew();
logger.Detail("WMI-Namespace: root\\cimv2");
logger.Detail("WQL: " + query);
try
{ {
foreach (ManagementObject item in searcher.Get()) using (var searcher = new ManagementObjectSearcher("root\\cimv2", query))
{ {
document.System.Properties.Add(new NameValueRecord("Betriebssystem", Value(item, "Caption"))); ConfigureSearcher(searcher);
document.System.Properties.Add(new NameValueRecord("Windows-Version", Value(item, "Version"))); using (var rows = searcher.Get())
document.System.Properties.Add(new NameValueRecord("Windows-Build", Value(item, "BuildNumber"))); {
document.System.Properties.Add(new NameValueRecord("Architektur", Value(item, "OSArchitecture"))); var count = 0;
foreach (ManagementObject item in rows)
{
using (item)
{
count++;
if (count != 1)
{
continue;
}
document.System.Properties.Add(new NameValueRecord(
"Betriebssystem",
Value(item, "Caption")));
document.System.Properties.Add(new NameValueRecord(
"Windows-Version",
Value(item, "Version")));
document.System.Properties.Add(new NameValueRecord(
"Windows-Build",
Value(item, "BuildNumber")));
document.System.Properties.Add(new NameValueRecord(
"Architektur",
Value(item, "OSArchitecture")));
document.System.Properties.Add(new NameValueRecord( document.System.Properties.Add(new NameValueRecord(
"Letzter Systemstart", "Letzter Systemstart",
ConvertWmiDate(Value(item, "LastBootUpTime")))); ConvertWmiDate(Value(item, "LastBootUpTime"))));
break;
} }
} }
timer.Stop();
logger.Info("Windows-WMI-Abfrage abgeschlossen: Datensätze="
+ count + "; Dauer=" + timer.ElapsedMilliseconds + " ms.");
}
}
}
catch (Exception exception) when (IsRecoverableWmiException(exception))
{
timer.Stop();
document.Findings.Add(new Finding
{
Severity = "Warnung",
Area = "Windows WMI",
Message = "Win32_OperatingSystem konnte nicht gelesen werden: "
+ exception.Message,
RecommendedAction =
"Lokalen WMI-Dienst und Leseberechtigungen prüfen; "
+ "die BizTalk-Erfassung wird fortgesetzt.",
Owner = "Windows-/BizTalk-Betrieb"
});
logger.Exception("Windows-WMI-Abfrage fehlgeschlagen", exception);
}
} }
private static void CollectBizTalkRegistry(InventoryDocument document) private void CollectBizTalkRegistry(InventoryDocument document)
{ {
foreach (var view in new[] { RegistryView.Registry64, RegistryView.Registry32 }) foreach (var view in new[] { RegistryView.Registry64, RegistryView.Registry32 })
{
logger.Detail("Prüfe BizTalk-Registryansicht: " + view + ".");
try
{ {
using (var baseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, view)) using (var baseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, view))
using (var product = baseKey.OpenSubKey(BizTalkKey, false)) using (var product = baseKey.OpenSubKey(BizTalkKey, false))
@@ -156,6 +226,24 @@ namespace BizTalkSapEnvironmentInventory.Collectors
} }
} }
} }
catch (Exception exception) when (IsRecoverableRegistryException(exception))
{
document.Findings.Add(new Finding
{
Severity = "Hinweis",
Area = "BizTalk Registry " + view,
Message = "BizTalk-Registryansicht konnte nicht gelesen werden: "
+ exception.Message,
RecommendedAction =
"Berechtigungen prüfen; WMI oder explizite "
+ "--management-Parameter können die Ermittlung übernehmen.",
Owner = "Windows-/BizTalk-Betrieb"
});
logger.Exception(
"BizTalk-Registryansicht " + view + " fehlgeschlagen",
exception);
}
}
if (string.IsNullOrWhiteSpace(document.System.BizTalkInstallPath)) if (string.IsNullOrWhiteSpace(document.System.BizTalkInstallPath))
{ {
@@ -170,18 +258,36 @@ namespace BizTalkSapEnvironmentInventory.Collectors
} }
} }
private static void CollectBizTalkGroupSetting(InventoryDocument document) private void CollectBizTalkGroupSetting(InventoryDocument document)
{ {
const string query =
"SELECT Name,MgmtDbServerName,MgmtDbName FROM MSBTS_GroupSetting";
var timer = Stopwatch.StartNew();
logger.Detail(@"WMI-Namespace: \\.\root\MicrosoftBizTalkServer");
logger.Detail("WQL: " + query);
try try
{ {
var scope = CreateBizTalkScope();
logger.Info("Verbinde lokal mit dem BizTalk-WMI-Namespace.");
scope.Connect();
using (var searcher = new ManagementObjectSearcher( using (var searcher = new ManagementObjectSearcher(
@"root\MicrosoftBizTalkServer", scope,
"SELECT Name,MgmtDbServerName,MgmtDbName,BizTalkAdministratorGroup," new ObjectQuery(query),
+ "BizTalkOperatorGroup,BizTalkReadOnlyUserGroup,SSOServerName " CreateEnumerationOptions()))
+ "FROM MSBTS_GroupSetting"))
{ {
foreach (ManagementObject item in searcher.Get()) using (var rows = searcher.Get())
{ {
var count = 0;
foreach (ManagementObject item in rows)
{
using (item)
{
count++;
if (count != 1)
{
continue;
}
var managementServer = Value(item, "MgmtDbServerName"); var managementServer = Value(item, "MgmtDbServerName");
var managementDatabase = Value(item, "MgmtDbName"); var managementDatabase = Value(item, "MgmtDbName");
if (!string.IsNullOrWhiteSpace(managementServer)) if (!string.IsNullOrWhiteSpace(managementServer))
@@ -194,38 +300,94 @@ namespace BizTalkSapEnvironmentInventory.Collectors
} }
AddWmiValue(document, item, "Name", "BizTalk Gruppenname"); AddWmiValue(document, item, "Name", "BizTalk Gruppenname");
AddWmiValue( }
}
timer.Stop();
logger.Info("MSBTS_GroupSetting abgeschlossen: Datensätze="
+ count + "; Dauer=" + timer.ElapsedMilliseconds + " ms.");
if (count == 0)
{
AddGroupSettingFinding(
document, document,
item, "MSBTS_GroupSetting lieferte keinen Datensatz.");
"BizTalkAdministratorGroup", }
"BizTalk Administratorengruppe"); else if (count > 1)
AddWmiValue( {
AddGroupSettingFinding(
document, document,
item, "MSBTS_GroupSetting lieferte "
"BizTalkOperatorGroup", + count
"BizTalk Operatorengruppe"); + " Datensätze; der erste Datensatz wurde verwendet.");
AddWmiValue(
document,
item,
"BizTalkReadOnlyUserGroup",
"BizTalk ReadOnly-Gruppe");
AddWmiValue(document, item, "SSOServerName", "Enterprise SSO Server");
break;
} }
} }
} }
catch (ManagementException exception) }
catch (Exception exception) when (IsRecoverableWmiException(exception))
{
timer.Stop();
AddGroupSettingFinding(
document,
"MSBTS_GroupSetting konnte für die Management-DB-Erkennung "
+ "nicht gelesen werden: " + exception.Message);
logger.Exception("MSBTS_GroupSetting fehlgeschlagen", exception);
}
}
private ManagementScope CreateBizTalkScope()
{
var scope = new ManagementScope(@"\\.\root\MicrosoftBizTalkServer");
scope.Options.Timeout = timeout;
scope.Options.EnablePrivileges = false;
scope.Options.Impersonation = ImpersonationLevel.Impersonate;
return scope;
}
private EnumerationOptions CreateEnumerationOptions()
{
return new EnumerationOptions
{
ReturnImmediately = false,
Rewindable = false,
Timeout = timeout
};
}
private void ConfigureSearcher(ManagementObjectSearcher searcher)
{
searcher.Options.ReturnImmediately = false;
searcher.Options.Rewindable = false;
searcher.Options.Timeout = timeout;
}
private static void AddGroupSettingFinding(
InventoryDocument document,
string message)
{ {
document.Findings.Add(new Finding document.Findings.Add(new Finding
{ {
Severity = "Hinweis", Severity = "Hinweis",
Area = "BizTalk GroupSetting", Area = "BizTalk GroupSetting",
Message = "MSBTS_GroupSetting konnte für die Management-DB-Erkennung nicht gelesen werden: " Message = message,
+ exception.Message, RecommendedAction =
RecommendedAction = "Registry-Ergebnis prüfen oder --management-server explizit angeben.", "Registry-Ergebnis prüfen oder --management-server und "
+ "--management-database explizit angeben.",
Owner = "BizTalk-Betrieb" Owner = "BizTalk-Betrieb"
}); });
} }
private static bool IsRecoverableWmiException(Exception exception)
{
return exception is ManagementException
|| exception is COMException
|| exception is UnauthorizedAccessException;
}
private static bool IsRecoverableRegistryException(Exception exception)
{
return exception is IOException
|| exception is SecurityException
|| exception is UnauthorizedAccessException;
} }
private static void AddWmiValue( private static void AddWmiValue(
@@ -1,6 +1,7 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Reflection;
using System.Text; using System.Text;
namespace BizTalkSapEnvironmentInventory.Infrastructure namespace BizTalkSapEnvironmentInventory.Infrastructure
@@ -26,6 +27,11 @@ namespace BizTalkSapEnvironmentInventory.Infrastructure
Write("INFO", message); Write("INFO", message);
} }
public void Detail(string message)
{
Write("DETAIL", message);
}
public void Warning(string message) public void Warning(string message)
{ {
Write("WARNUNG", message); Write("WARNUNG", message);
@@ -36,6 +42,62 @@ namespace BizTalkSapEnvironmentInventory.Infrastructure
Write("FEHLER", message); Write("FEHLER", message);
} }
/// <summary>
/// Protokolliert die vollständige Ausnahmekette einschließlich HRESULT,
/// WMI-Status und Stacktrace, damit Providerfehler vor Ort analysierbar bleiben.
/// </summary>
public void Exception(string context, Exception exception)
{
if (exception == null)
{
Error(context + ": Unbekannter Fehler ohne Ausnahmeobjekt.");
return;
}
Error(context + ": " + exception.Message);
var current = exception;
var depth = 0;
while (current != null)
{
Detail(string.Format(
CultureInfo.InvariantCulture,
"Ausnahme[{0}]: Typ={1}; HRESULT=0x{2:X8}; Meldung={3}",
depth,
current.GetType().FullName,
current.HResult,
current.Message));
if (string.Equals(
current.GetType().FullName,
"System.Management.ManagementException",
StringComparison.Ordinal))
{
var errorCodeProperty = current.GetType().GetProperty(
"ErrorCode",
BindingFlags.Instance | BindingFlags.Public);
if (errorCodeProperty != null)
{
Detail("WMI-Status: " + Convert.ToString(
errorCodeProperty.GetValue(current, null),
CultureInfo.InvariantCulture));
}
}
if (!string.IsNullOrWhiteSpace(current.StackTrace))
{
Detail("Stacktrace[" + depth + "]:" + Environment.NewLine
+ current.StackTrace);
}
// Reflection kapselt ExplorerOM-Fehler häufig in TargetInvocationException.
var targetInvocation = current as TargetInvocationException;
current = targetInvocation != null && targetInvocation.InnerException != null
? targetInvocation.InnerException
: current.InnerException;
depth++;
}
}
public void Dispose() public void Dispose()
{ {
writer.Dispose(); writer.Dispose();
@@ -58,10 +58,12 @@ namespace BizTalkSapEnvironmentInventory.Infrastructure
Severity = required ? "Fehler" : "Warnung", Severity = required ? "Fehler" : "Warnung",
Area = name, Area = name,
Message = "Datenerfassung fehlgeschlagen: " + exception.Message, Message = "Datenerfassung fehlgeschlagen: " + exception.Message,
RecommendedAction = "Logdatei, Berechtigungen und lokale BizTalk-Konfiguration prüfen.", RecommendedAction =
"Logdatei, WMI-Provider, ExplorerOM-Installation, Berechtigungen "
+ "und lokale BizTalk-Konfiguration prüfen.",
Owner = "BizTalk-Betrieb" Owner = "BizTalk-Betrieb"
}); });
logger.Error(name + ": " + exception.Message); logger.Exception(name, exception);
} }
} }
} }
@@ -2,6 +2,7 @@ using System;
using System.IO; using System.IO;
using System.IO.Compression; using System.IO.Compression;
using System.Linq; using System.Linq;
using System.Reflection;
using System.Text; using System.Text;
using System.Xml.Linq; using System.Xml.Linq;
using BizTalkSapEnvironmentInventory.Collectors; using BizTalkSapEnvironmentInventory.Collectors;
@@ -23,6 +24,7 @@ namespace BizTalkSapEnvironmentInventory.Infrastructure
{ {
TestSanitizer(); TestSanitizer();
TestBindingParser(); TestBindingParser();
TestLoggerDiagnostics();
TestDocxPackage(); TestDocxPackage();
} }
@@ -236,6 +238,47 @@ namespace BizTalkSapEnvironmentInventory.Infrastructure
} }
} }
private static void TestLoggerDiagnostics()
{
var directory = CreateTemporaryDirectory();
var path = Path.Combine(directory, "diagnostics.log");
try
{
var exception = new TargetInvocationException(
new InvalidOperationException("Innerer Diagnosefehler"));
using (var logger = new ConsoleFileLogger(path))
{
var originalOutput = Console.Out;
try
{
// Die absichtlich erzeugte Testausnahme soll den Self-Test
// auf der Konsole nicht wie einen echten Laufzeitfehler aussehen lassen.
Console.SetOut(TextWriter.Null);
logger.Exception("Self-Test-Diagnose", exception);
}
finally
{
Console.SetOut(originalOutput);
}
}
var log = File.ReadAllText(path);
Assert(
log.Contains(typeof(TargetInvocationException).FullName),
"Logger dokumentierte die Reflection-Ausnahme nicht.");
Assert(
log.Contains(typeof(InvalidOperationException).FullName),
"Logger dokumentierte die innere Ausnahme nicht.");
Assert(
log.Contains("HRESULT=0x"),
"Logger dokumentierte den HRESULT nicht.");
}
finally
{
DeleteDirectory(directory);
}
}
private static void AssertPackageNamespaces(ZipArchive archive) private static void AssertPackageNamespaces(ZipArchive archive)
{ {
var contentTypes = LoadEntryXml(archive, "[Content_Types].xml"); var contentTypes = LoadEntryXml(archive, "[Content_Types].xml");
+33 -7
View File
@@ -82,21 +82,36 @@ namespace BizTalkSapEnvironmentInventory
logger.Info("Umgebung: " + options.EnvironmentName); logger.Info("Umgebung: " + options.EnvironmentName);
logger.Info("Server: " + Environment.MachineName); logger.Info("Server: " + Environment.MachineName);
logger.Info("Ausgabeordner: " + options.OutputDirectory); logger.Info("Ausgabeordner: " + options.OutputDirectory);
logger.Info("Modus: read-only"); logger.Info(
"Modus: read-only; ExplorerOM für Anwendungen, WMI für "
+ "Gruppenermittlung und optionale Artefakte.");
logger.Detail("Logdatei: " + logPath);
logger.Detail("DOCX-Zieldatei: " + reportPath);
var wmiTimeoutSeconds = ReadIntSetting("WmiTimeoutSeconds", 30);
var processTimeoutSeconds = ReadIntSetting("ProcessTimeoutSeconds", 120);
var maxArtifactDetails = ReadIntSetting("MaxArtifactDetailsPerType", 5000);
logger.Detail("Konfiguration WmiTimeoutSeconds=" + wmiTimeoutSeconds + ".");
logger.Detail("Konfiguration ProcessTimeoutSeconds=" + processTimeoutSeconds + ".");
logger.Detail("Konfiguration MaxArtifactDetailsPerType="
+ maxArtifactDetails + ".");
var safeCollector = new SafeCollector(document, logger); var safeCollector = new SafeCollector(document, logger);
safeCollector.Execute( safeCollector.Execute(
"System und BizTalk-Installation", "System und BizTalk-Installation",
true, true,
() => new SystemCollector(options).Collect(document)); () => new SystemCollector(
options,
logger,
wmiTimeoutSeconds).Collect(document));
safeCollector.Execute( safeCollector.Execute(
"BizTalk-Anwendungen und WMI-Artefakte", "BizTalk-Anwendungen und optionale WMI-Artefakte",
true, true,
() => new BizTalkWmiCollector( () => new BizTalkWmiCollector(
document, document,
logger, logger,
ReadIntSetting("WmiTimeoutSeconds", 30), wmiTimeoutSeconds,
ReadIntSetting("MaxArtifactDetailsPerType", 5000)).Collect()); maxArtifactDetails).Collect());
safeCollector.Execute( safeCollector.Execute(
"SAP-Bindingparameter", "SAP-Bindingparameter",
true, true,
@@ -104,7 +119,7 @@ namespace BizTalkSapEnvironmentInventory
options, options,
document, document,
logger, logger,
ReadIntSetting("ProcessTimeoutSeconds", 120)).Collect()); processTimeoutSeconds).Collect());
safeCollector.Execute( safeCollector.Execute(
"SAP NCo, SNC und Zertifikatsmetadaten", "SAP NCo, SNC und Zertifikatsmetadaten",
false, false,
@@ -125,11 +140,22 @@ namespace BizTalkSapEnvironmentInventory
} }
catch (Exception exception) catch (Exception exception)
{ {
logger.Error("Word-Dokument konnte nicht erzeugt werden: " + exception); logger.Exception(
"Word-Dokument konnte nicht erzeugt werden",
exception);
return 2; return 2;
} }
var exitCode = document.HasRequiredSectionFailure ? 1 : 0; var exitCode = document.HasRequiredSectionFailure ? 1 : 0;
logger.Info("Ergebnisübersicht: Anwendungen=" + document.Applications.Count
+ "; SAP-Endpunkte=" + document.SapEndpoints.Count
+ "; Findings=" + document.Findings.Count + ".");
if (exitCode != 0)
{
logger.Warning(
"Der Bericht wurde erzeugt, ist wegen mindestens eines "
+ "Pflichtfehlers aber unvollständig.");
}
logger.Info("Erfassung beendet. Exitcode: " + exitCode); logger.Info("Erfassung beendet. Exitcode: " + exitCode);
return exitCode; return exitCode;
} }
@@ -167,9 +167,10 @@ namespace BizTalkSapEnvironmentInventory.Reporting
WriteHeading(writer, "5. Vollständige BizTalk-Anwendungsliste", 1); WriteHeading(writer, "5. Vollständige BizTalk-Anwendungsliste", 1);
WriteParagraph( WriteParagraph(
writer, writer,
"Die Liste stammt primär aus MSBTS_Application. Artefaktzahlen werden aus den " "Die vollständige Anwendungsliste stammt aus dem read-only BizTalk Explorer "
+ "lokal verfügbaren BizTalk-WMI-Klassen gebildet; optionale WMI-Klassen können " + "Object Model. Artefaktzahlen werden aus lokal verfügbaren, optionalen "
+ "versionsabhängig fehlen und werden dann im Erfassungsstatus bzw. als Finding ausgewiesen.", + "BizTalk-WMI-Klassen gebildet; fehlende Klassen werden im Log und als "
+ "Finding ausgewiesen, ohne die Anwendungsliste zu verwerfen.",
"Normal", "Normal",
false, false,
null); null);