diff --git a/Dokumentation.md b/Dokumentation.md index 9ace5f7..072a4f7 100644 --- a/Dokumentation.md +++ b/Dokumentation.md @@ -38,13 +38,13 @@ Administrative cmd.exe +-- SystemCollector | +-- Windows WMI | +-- BizTalk Registry - | +-- Management SQL/DB + | +-- MSBTS_GroupSetting für Management SQL/DB | +-- BizTalkWmiCollector + | +-- ExplorerOM: vollständige Anwendungsliste | +-- root\MicrosoftBizTalkServer - | +-- Anwendungen und Artefakte - | +-- Adapter, Hosts und Handler - | +-- SAP-Endpunkt-Fallback + | +-- optionale Artefakte, Adapter, Hosts und Handler + | +-- optionaler SAP-Endpunkt-Fallback | +-- BindingExportCollector | +-- BTSTask ExportBindings /GroupLevel @@ -62,7 +62,13 @@ Administrative cmd.exe +-- 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 @@ -85,7 +91,30 @@ Ermittelt werden unter anderem: 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: @@ -93,11 +122,6 @@ Primärer Namespace: 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: - `MSBTS_Orchestration` @@ -115,13 +139,16 @@ Optionale beziehungsweise versionsabhängige Artefaktklassen: - `MSBTS_HostSetting` - `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` die aufgenommenen Detaildatensätze pro WMI-Klasse auf standardmäßig 5000. Die Anwendungsliste selbst wird nicht begrenzt. -### 3.3 BTSTask-Gruppenbinding +### 3.4 BTSTask-Gruppenbinding 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. -### 3.4 Offline-Binding +### 3.5 Offline-Binding Für Diagnose- oder Berechtigungsfälle kann ein vorhandener Export verarbeitet werden: @@ -306,8 +333,14 @@ Das DR-Runbook muss mindestens enthalten: ## 10. Resilienz - 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` +- live gespiegelte, secret-bereinigte BTSTask-Ausgabe - WMI-Fallback für SAP-Endpunkte - Offline-Bindingmodus - optionale WMI-Klassen stoppen den Lauf nicht @@ -317,6 +350,8 @@ Das DR-Runbook muss mindestens enthalten: - atomare DOCX-Ausgabe - eindeutige Dateinamen mit Umgebung, Server und Zeitstempel - 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 @@ -343,10 +378,12 @@ Auf jedem der beiden BizTalk Server: 3. `BizTalkSapEnvironmentInventory.exe --self-test` ausführen. 4. Erfassung starten. 5. Exitcode prüfen. -6. DOCX und Log gemeinsam kontrollieren. -7. Anwendungsliste gegen BizTalk Administration abgleichen. -8. SAP-Endpunkte und Hostzuordnung prüfen. -9. WE20-/WE21-/Credential-/DR-Findings an SAP Basis geben. +6. Im Log `MSBTS_GroupSetting`, Datensatzanzahl und Management-Datenbankziel prüfen. +7. ExplorerOM-Assemblyname, Version und Pfad kontrollieren. +8. DOCX und Log gemeinsam kontrollieren. +9. Anwendungsliste gegen BizTalk Administration abgleichen. +10. SAP-Endpunkte und Hostzuordnung prüfen. +11. WE20-/WE21-/Credential-/DR-Findings an SAP Basis geben. ACC: @@ -382,6 +419,7 @@ Tests prüfen: - Parsing eines WCF-SAP-Gruppenbindings - SAP-Client, Host und `UseSnc` - Anwendungszuordnung +- Diagnose-Logging mit HRESULT und innerer Reflection-Ausnahme - DOCX-Paketstruktur - XML-Gültigkeit aller DOCX-Parts - Ausschluss von Testkennwörtern @@ -395,12 +433,15 @@ Tests prüfen: - keine Garantie, dass Schemanamen alle verwendeten IDoc-Erweiterungen enthalten - dynamisch zur Laufzeit konstruierte SAP-Adressen können im statischen Binding fehlen - 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 ## 15. Referenzen - Microsoft: BizTalk Server 2020 Hardware and Software Requirements - Microsoft: `BTSTask ExportBindings` +- Microsoft: BizTalk Explorer Object Model - Microsoft: BizTalk WMI Technical Reference - Microsoft: WCF-SAP Port Configuration - Microsoft: SAP System Connection URI diff --git a/Readme.md b/Readme.md index 32bb8dd..5f37797 100644 --- a/Readme.md +++ b/Readme.md @@ -7,14 +7,18 @@ Das Tool wird einmal auf dem BizTalk Server in `ACC` und einmal auf dem BizTalk - einem BizTalk Server 2020 - 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 ### Vollständige BizTalk-Anwendungsliste -- alle Anwendungen aus `MSBTS_Application` -- Anwendungsstatus und Beschreibung +- alle Anwendungen read-only aus dem BizTalk Explorer Object Model +- Anwendungsstatus und Beschreibung, soweit ExplorerOM sie bereitstellt - Anzahl und Zuordnung von Orchestrierungen - Send Ports und Send Port Groups - Receive Ports und Receive Locations @@ -69,7 +73,7 @@ Das Tool erfindet hierfür keine Werte. Es dokumentiert nur lokal belegbare Indi ## 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 - temporärer Binding-Export wird nach dem Parsen gelöscht - 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 - .NET Framework 4.7.2 oder höher - Zugriff auf die konfigurierte BizTalk Management Database +- lokale BizTalk-Verwaltungskomponenten einschließlich + `Microsoft.BizTalk.ExplorerOM.dll` - `BTSTask.exe` aus der lokalen BizTalk-Installation ## Schnellstart @@ -127,15 +133,25 @@ BizTalkSapEnvironmentInventory.exe ^ Das Tool zeigt jeden Abschnitt und die erzeugten Dateien direkt auf der Konsole an: ```text -[INFO] Starte Abschnitt: BizTalk-Anwendungen und WMI-Artefakte -[INFO] 34 BizTalk-Anwendung(en) über WMI gefunden. +[INFO] Ermittle die BizTalk-Gruppe über MSBTS_GroupSetting. +[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] BTSTask ExportBindings erfolgreich abgeschlossen (1865 ms). [INFO] 12 SAP-Endpunkt(e) aus dem Binding-Export ausgewertet. -[INFO] Erzeuge Microsoft-Word-Dokument: C:\BizTalk-Doku\ACC\... -[INFO] Word-Dokument erfolgreich erzeugt: C:\BizTalk-Doku\ACC\... +[INFO] Ergebnisübersicht: Anwendungen=34; SAP-Endpunkte=12; Findings=4. ``` -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 @@ -200,7 +216,28 @@ Die Gitea-Workflowdatei `.gitea/workflows/build.yml` baut, testet und veröffent - lokal auf dem BizTalk Server ausführen - 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 @@ -228,4 +265,3 @@ BizTalkSapEnvironmentInventory.exe ^ - DOCX-Findings und Log prüfen Weitere Architektur- und Sicherheitsdetails stehen in [Dokumentation.md](Dokumentation.md). - diff --git a/src/BizTalkSapEnvironmentInventory/Collectors/BindingExportCollector.cs b/src/BizTalkSapEnvironmentInventory/Collectors/BindingExportCollector.cs index 0d1c4fb..9dcb2ff 100644 --- a/src/BizTalkSapEnvironmentInventory/Collectors/BindingExportCollector.cs +++ b/src/BizTalkSapEnvironmentInventory/Collectors/BindingExportCollector.cs @@ -530,7 +530,13 @@ namespace BizTalkSapEnvironmentInventory.Collectors } 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 outputSync = new object(); + var timer = Stopwatch.StartNew(); var startInfo = new ProcessStartInfo { FileName = btsTaskPath, @@ -548,14 +554,24 @@ namespace BizTalkSapEnvironmentInventory.Collectors { 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) => { 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."); } process.WaitForExit(); + timer.Stop(); if (process.ExitCode != 0) { + string errorOutput; + lock (outputSync) + { + errorOutput = output.ToString(); + } throw new InvalidOperationException( "BTSTask ExportBindings meldete 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) { 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() diff --git a/src/BizTalkSapEnvironmentInventory/Collectors/BizTalkWmiCollector.cs b/src/BizTalkSapEnvironmentInventory/Collectors/BizTalkWmiCollector.cs index bf85bc4..20b378d 100644 --- a/src/BizTalkSapEnvironmentInventory/Collectors/BizTalkWmiCollector.cs +++ b/src/BizTalkSapEnvironmentInventory/Collectors/BizTalkWmiCollector.cs @@ -1,19 +1,29 @@ using System; +using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; +using System.IO; using System.Linq; using System.Management; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Security.Principal; using BizTalkSapEnvironmentInventory.Infrastructure; using BizTalkSapEnvironmentInventory.Models; namespace BizTalkSapEnvironmentInventory.Collectors { /// - /// 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. /// 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 ConsoleFileLogger logger; @@ -31,18 +41,104 @@ namespace BizTalkSapEnvironmentInventory.Collectors this.logger = logger; timeout = TimeSpan.FromSeconds(Math.Max(5, timeoutSeconds)); this.maxArtifactDetailsPerType = Math.Max(100, maxArtifactDetailsPerType); - scope = new ManagementScope(@"\\" + Environment.MachineName + @"\" + NamespacePath); + scope = new ManagementScope(WmiNamespace); scope.Options.Timeout = timeout; + scope.Options.EnablePrivileges = false; + scope.Options.Impersonation = ImpersonationLevel.Impersonate; } /// - /// 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. /// public void Collect() { - scope.Connect(); + LogRuntimeInformation(); 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[] { new ClassDescriptor("MSBTS_Orchestration", "Orchestration"), @@ -72,41 +168,210 @@ namespace BizTalkSapEnvironmentInventory.Collectors TryCollectWmiSapEndpoints("MSBTS_SendPort", "Senden"); 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"); - foreach (var row in rows) + logger.Info("Diagnose: Computer=" + Environment.MachineName + + "; 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"); - if (string.IsNullOrWhiteSpace(name)) + using (var identity = WindowsIdentity.GetCurrent()) + { + 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(); + 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; } - GetOrCreateApplication(name).Description = First(row, "Description"); - GetOrCreateApplication(name).Status = FormatStatus(First(row, "Status")); + try + { + return Assembly.LoadFrom(path); + } + catch (Exception exception) + { + logger.Detail("Assembly.LoadFrom fehlgeschlagen: " + + exception.GetType().Name + ": " + exception.Message); + } } - if (document.Applications.Count == 0) + return null; + } + + private void LogExplorerAssembly(Assembly assembly) + { + logger.Info("ExplorerOM-Assembly geladen: " + assembly.FullName + "."); + try { - throw new InvalidOperationException( - "WMI lieferte keine BizTalk-Anwendungen. Berechtigungen und BizTalk-Gruppe prüfen."); + logger.Detail("ExplorerOM-Assemblypfad: " + assembly.Location); + } + catch (NotSupportedException) + { + logger.Detail( + "ExplorerOM-Assemblypfad ist für diese Assembly nicht verfügbar."); } - - logger.Info(document.Applications.Count + " BizTalk-Anwendung(en) über WMI gefunden."); } private void TryCollectArtifacts(ClassDescriptor descriptor) { + List rows = null; try { 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) { @@ -164,10 +429,14 @@ namespace BizTalkSapEnvironmentInventory.Collectors logger.Info(descriptor.DisplayName + ": " + count + " Artefakt(e)."); } - catch (ManagementException exception) + catch (Exception exception) when (IsRecoverableWmiException(exception)) { AddOptionalWmiFinding(descriptor.ClassName, exception); } + finally + { + DisposeRows(rows); + } } private bool TryCollectComponents( @@ -176,9 +445,11 @@ namespace BizTalkSapEnvironmentInventory.Collectors List target, bool onlySap) { + List rows = null; try { - foreach (var row in Query("SELECT * FROM " + className)) + rows = Query(category, "SELECT * FROM " + className); + foreach (var row in rows) { if (onlySap && !LooksLikeSap(row)) { @@ -199,18 +470,26 @@ namespace BizTalkSapEnvironmentInventory.Collectors } return true; } - catch (ManagementException exception) + catch (Exception exception) when (IsRecoverableWmiException(exception)) { AddOptionalWmiFinding(className, exception); return false; } + finally + { + DisposeRows(rows); + } } private void TryCollectWmiSapEndpoints(string className, string direction) { + List rows = null; 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)) { @@ -249,13 +528,17 @@ namespace BizTalkSapEnvironmentInventory.Collectors document.SapEndpoints.Add(endpoint); } } - catch (ManagementException exception) + catch (Exception exception) when (IsRecoverableWmiException(exception)) { AddOptionalWmiFinding(className + " SAP-Fallback", exception); } + finally + { + DisposeRows(rows); + } } - private List Query(string query) + private List Query(string name, string query) { var options = new EnumerationOptions { @@ -263,15 +546,125 @@ namespace BizTalkSapEnvironmentInventory.Collectors Rewindable = false, Timeout = timeout }; + var timer = Stopwatch.StartNew(); + logger.Info("WMI-Abfrage startet: " + name + "."); + logger.Detail("WQL: " + query); using (var searcher = new ManagementObjectSearcher( scope, new ObjectQuery(query), options)) { - return searcher.Get().Cast().ToList(); + using (var rows = searcher.Get()) + { + var result = rows.Cast().ToList(); + timer.Stop(); + logger.Info("WMI-Abfrage abgeschlossen: " + name + + "; Datensätze=" + result.Count + + "; Dauer=" + timer.ElapsedMilliseconds + " ms."); + return result; + } } } + private static IList 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(); + 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) { var application = document.Applications.FirstOrDefault(item => @@ -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 { @@ -415,7 +808,32 @@ namespace BizTalkSapEnvironmentInventory.Collectors RecommendedAction = "Nur relevant, wenn die zugehörige Artefaktspalte vollständig benötigt wird.", 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 rows) + { + if (rows == null) + { + return; + } + + foreach (var row in rows) + { + if (row != null) + { + row.Dispose(); + } + } } private sealed class ClassDescriptor diff --git a/src/BizTalkSapEnvironmentInventory/Collectors/SapRuntimeCollector.cs b/src/BizTalkSapEnvironmentInventory/Collectors/SapRuntimeCollector.cs index 6fb45e9..eb8d8aa 100644 --- a/src/BizTalkSapEnvironmentInventory/Collectors/SapRuntimeCollector.cs +++ b/src/BizTalkSapEnvironmentInventory/Collectors/SapRuntimeCollector.cs @@ -46,15 +46,32 @@ namespace BizTalkSapEnvironmentInventory.Collectors /// public void Collect() { + logger.Info("Prüfe installierte SAP-/BizTalk-Adapter-Komponenten."); CollectInstalledProducts(); + logger.Info("Lese konfigurierte SNC- und SAP-Runtime-Pfade."); var configuredPaths = CollectConfiguredSecurityPaths(); + logger.Info("Lese SAP-relevante Umgebungsvariablen."); CollectEnvironment(document.RuntimeComponents); + logger.Info("Suche SAP-Runtime-Dateien in begrenzten Installationspfaden."); CollectRuntimeFiles(configuredPaths); + logger.Info("Prüfe sichere Metadaten lokaler PSE-/Security-Dateien."); CollectSecurityFiles(configuredPaths); if (includeCertificates) { + logger.Info("Lese SAP-relevante Zertifikatsmetadaten."); 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 => string.Equals( diff --git a/src/BizTalkSapEnvironmentInventory/Collectors/SystemCollector.cs b/src/BizTalkSapEnvironmentInventory/Collectors/SystemCollector.cs index a9872b2..7148d65 100644 --- a/src/BizTalkSapEnvironmentInventory/Collectors/SystemCollector.cs +++ b/src/BizTalkSapEnvironmentInventory/Collectors/SystemCollector.cs @@ -1,9 +1,14 @@ using System; +using System.Diagnostics; using System.Globalization; +using System.IO; using System.Management; using System.Reflection; +using System.Runtime.InteropServices; +using System.Security; using Microsoft.Win32; using BizTalkSapEnvironmentInventory.Configuration; +using BizTalkSapEnvironmentInventory.Infrastructure; using BizTalkSapEnvironmentInventory.Models; namespace BizTalkSapEnvironmentInventory.Collectors @@ -17,10 +22,17 @@ namespace BizTalkSapEnvironmentInventory.Collectors private const string AdministrationKey = BizTalkKey + @"\Administration"; 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.logger = logger; + timeout = TimeSpan.FromSeconds(Math.Max(5, timeoutSeconds)); } /// @@ -43,8 +55,11 @@ namespace BizTalkSapEnvironmentInventory.Collectors ".NET Runtime", Environment.Version.ToString())); + logger.Info("Lese lokale Windows-Betriebssysteminformationen."); CollectOperatingSystem(document); + logger.Info("Lese lokale BizTalk-Installation aus der Registry."); CollectBizTalkRegistry(document); + logger.Info("Ermittle die BizTalk-Gruppe über MSBTS_GroupSetting."); CollectBizTalkGroupSetting(document); if (!string.IsNullOrWhiteSpace(options.ManagementServer)) @@ -67,6 +82,11 @@ namespace BizTalkSapEnvironmentInventory.Collectors document.System.Properties.Add(new NameValueRecord( "BizTalk Management Database", 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) && !string.Equals(document.EnvironmentName, "PROD", StringComparison.OrdinalIgnoreCase)) @@ -82,78 +102,146 @@ namespace BizTalkSapEnvironmentInventory.Collectors } } - private static void CollectOperatingSystem(InventoryDocument document) + private void CollectOperatingSystem(InventoryDocument document) { - using (var searcher = new ManagementObjectSearcher( - "root\\cimv2", - "SELECT Caption,Version,BuildNumber,OSArchitecture,LastBootUpTime FROM Win32_OperatingSystem")) + const string query = + "SELECT Caption,Version,BuildNumber,OSArchitecture,LastBootUpTime " + + "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"))); - 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( - "Letzter Systemstart", - ConvertWmiDate(Value(item, "LastBootUpTime")))); - break; + ConfigureSearcher(searcher); + using (var rows = searcher.Get()) + { + 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( + "Letzter Systemstart", + ConvertWmiDate(Value(item, "LastBootUpTime")))); + } + } + 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 }) { - using (var baseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, view)) - using (var product = baseKey.OpenSubKey(BizTalkKey, false)) + logger.Detail("Prüfe BizTalk-Registryansicht: " + view + "."); + try { - if (product != null) + using (var baseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, view)) + using (var product = baseKey.OpenSubKey(BizTalkKey, false)) { - AddRegistryValue(document, product, "ProductVersion", "BizTalk Produktversion", view); - AddRegistryValue(document, product, "ProductName", "BizTalk Produktname", view); - AddRegistryValue(document, product, "Edition", "BizTalk Edition", view); - var installPath = FirstRegistryValue( - product, - "InstallPath", - "BizTalkServerInstallPath", - "Path"); - if (!string.IsNullOrWhiteSpace(installPath) - && string.IsNullOrWhiteSpace(document.System.BizTalkInstallPath)) + if (product != null) { - document.System.BizTalkInstallPath = installPath; - document.System.Properties.Add(new NameValueRecord( - "BizTalk Installationspfad", - installPath, - "Registry " + view)); + AddRegistryValue(document, product, "ProductVersion", "BizTalk Produktversion", view); + AddRegistryValue(document, product, "ProductName", "BizTalk Produktname", view); + AddRegistryValue(document, product, "Edition", "BizTalk Edition", view); + var installPath = FirstRegistryValue( + product, + "InstallPath", + "BizTalkServerInstallPath", + "Path"); + if (!string.IsNullOrWhiteSpace(installPath) + && string.IsNullOrWhiteSpace(document.System.BizTalkInstallPath)) + { + document.System.BizTalkInstallPath = installPath; + document.System.Properties.Add(new NameValueRecord( + "BizTalk Installationspfad", + installPath, + "Registry " + view)); + } + } + } + + using (var baseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, view)) + using (var administration = baseKey.OpenSubKey(AdministrationKey, false)) + { + if (administration == null) + { + continue; + } + + if (string.IsNullOrWhiteSpace(document.System.ManagementServer)) + { + document.System.ManagementServer = FirstRegistryValue( + administration, + "MgmtDBServer", + "ManagementDBServer"); + } + + if (string.IsNullOrWhiteSpace(document.System.ManagementDatabase)) + { + document.System.ManagementDatabase = FirstRegistryValue( + administration, + "MgmtDBName", + "ManagementDBName"); } } } - - using (var baseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, view)) - using (var administration = baseKey.OpenSubKey(AdministrationKey, false)) + catch (Exception exception) when (IsRecoverableRegistryException(exception)) { - if (administration == null) + document.Findings.Add(new Finding { - continue; - } - - if (string.IsNullOrWhiteSpace(document.System.ManagementServer)) - { - document.System.ManagementServer = FirstRegistryValue( - administration, - "MgmtDBServer", - "ManagementDBServer"); - } - - if (string.IsNullOrWhiteSpace(document.System.ManagementDatabase)) - { - document.System.ManagementDatabase = FirstRegistryValue( - administration, - "MgmtDBName", - "ManagementDBName"); - } + 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); } } @@ -170,64 +258,138 @@ 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 { + var scope = CreateBizTalkScope(); + logger.Info("Verbinde lokal mit dem BizTalk-WMI-Namespace."); + scope.Connect(); using (var searcher = new ManagementObjectSearcher( - @"root\MicrosoftBizTalkServer", - "SELECT Name,MgmtDbServerName,MgmtDbName,BizTalkAdministratorGroup," - + "BizTalkOperatorGroup,BizTalkReadOnlyUserGroup,SSOServerName " - + "FROM MSBTS_GroupSetting")) + scope, + new ObjectQuery(query), + CreateEnumerationOptions())) { - foreach (ManagementObject item in searcher.Get()) + using (var rows = searcher.Get()) { - var managementServer = Value(item, "MgmtDbServerName"); - var managementDatabase = Value(item, "MgmtDbName"); - if (!string.IsNullOrWhiteSpace(managementServer)) + var count = 0; + foreach (ManagementObject item in rows) { - document.System.ManagementServer = managementServer; - } - if (!string.IsNullOrWhiteSpace(managementDatabase)) - { - document.System.ManagementDatabase = managementDatabase; + using (item) + { + count++; + if (count != 1) + { + continue; + } + + var managementServer = Value(item, "MgmtDbServerName"); + var managementDatabase = Value(item, "MgmtDbName"); + if (!string.IsNullOrWhiteSpace(managementServer)) + { + document.System.ManagementServer = managementServer; + } + if (!string.IsNullOrWhiteSpace(managementDatabase)) + { + document.System.ManagementDatabase = managementDatabase; + } + + AddWmiValue(document, item, "Name", "BizTalk Gruppenname"); + } } - AddWmiValue(document, item, "Name", "BizTalk Gruppenname"); - AddWmiValue( - document, - item, - "BizTalkAdministratorGroup", - "BizTalk Administratorengruppe"); - AddWmiValue( - document, - item, - "BizTalkOperatorGroup", - "BizTalk Operatorengruppe"); - AddWmiValue( - document, - item, - "BizTalkReadOnlyUserGroup", - "BizTalk ReadOnly-Gruppe"); - AddWmiValue(document, item, "SSOServerName", "Enterprise SSO Server"); - break; + timer.Stop(); + logger.Info("MSBTS_GroupSetting abgeschlossen: Datensätze=" + + count + "; Dauer=" + timer.ElapsedMilliseconds + " ms."); + if (count == 0) + { + AddGroupSettingFinding( + document, + "MSBTS_GroupSetting lieferte keinen Datensatz."); + } + else if (count > 1) + { + AddGroupSettingFinding( + document, + "MSBTS_GroupSetting lieferte " + + count + + " Datensätze; der erste Datensatz wurde verwendet."); + } } } } - catch (ManagementException exception) + catch (Exception exception) when (IsRecoverableWmiException(exception)) { - document.Findings.Add(new Finding - { - Severity = "Hinweis", - Area = "BizTalk GroupSetting", - Message = "MSBTS_GroupSetting konnte für die Management-DB-Erkennung nicht gelesen werden: " - + exception.Message, - RecommendedAction = "Registry-Ergebnis prüfen oder --management-server explizit angeben.", - Owner = "BizTalk-Betrieb" - }); + 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 + { + Severity = "Hinweis", + Area = "BizTalk GroupSetting", + Message = message, + RecommendedAction = + "Registry-Ergebnis prüfen oder --management-server und " + + "--management-database explizit angeben.", + 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( InventoryDocument document, ManagementBaseObject item, diff --git a/src/BizTalkSapEnvironmentInventory/Infrastructure/ConsoleFileLogger.cs b/src/BizTalkSapEnvironmentInventory/Infrastructure/ConsoleFileLogger.cs index d1e8410..5ec4e33 100644 --- a/src/BizTalkSapEnvironmentInventory/Infrastructure/ConsoleFileLogger.cs +++ b/src/BizTalkSapEnvironmentInventory/Infrastructure/ConsoleFileLogger.cs @@ -1,6 +1,7 @@ using System; using System.Globalization; using System.IO; +using System.Reflection; using System.Text; namespace BizTalkSapEnvironmentInventory.Infrastructure @@ -26,6 +27,11 @@ namespace BizTalkSapEnvironmentInventory.Infrastructure Write("INFO", message); } + public void Detail(string message) + { + Write("DETAIL", message); + } + public void Warning(string message) { Write("WARNUNG", message); @@ -36,6 +42,62 @@ namespace BizTalkSapEnvironmentInventory.Infrastructure Write("FEHLER", message); } + /// + /// Protokolliert die vollständige Ausnahmekette einschließlich HRESULT, + /// WMI-Status und Stacktrace, damit Providerfehler vor Ort analysierbar bleiben. + /// + 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() { writer.Dispose(); diff --git a/src/BizTalkSapEnvironmentInventory/Infrastructure/SafeCollector.cs b/src/BizTalkSapEnvironmentInventory/Infrastructure/SafeCollector.cs index a72b10c..303427d 100644 --- a/src/BizTalkSapEnvironmentInventory/Infrastructure/SafeCollector.cs +++ b/src/BizTalkSapEnvironmentInventory/Infrastructure/SafeCollector.cs @@ -58,10 +58,12 @@ namespace BizTalkSapEnvironmentInventory.Infrastructure Severity = required ? "Fehler" : "Warnung", Area = name, 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" }); - logger.Error(name + ": " + exception.Message); + logger.Exception(name, exception); } } } diff --git a/src/BizTalkSapEnvironmentInventory/Infrastructure/SelfTestRunner.cs b/src/BizTalkSapEnvironmentInventory/Infrastructure/SelfTestRunner.cs index 5652ab9..e3848a8 100644 --- a/src/BizTalkSapEnvironmentInventory/Infrastructure/SelfTestRunner.cs +++ b/src/BizTalkSapEnvironmentInventory/Infrastructure/SelfTestRunner.cs @@ -2,6 +2,7 @@ using System; using System.IO; using System.IO.Compression; using System.Linq; +using System.Reflection; using System.Text; using System.Xml.Linq; using BizTalkSapEnvironmentInventory.Collectors; @@ -23,6 +24,7 @@ namespace BizTalkSapEnvironmentInventory.Infrastructure { TestSanitizer(); TestBindingParser(); + TestLoggerDiagnostics(); 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) { var contentTypes = LoadEntryXml(archive, "[Content_Types].xml"); diff --git a/src/BizTalkSapEnvironmentInventory/Program.cs b/src/BizTalkSapEnvironmentInventory/Program.cs index 64ddd8a..107c197 100644 --- a/src/BizTalkSapEnvironmentInventory/Program.cs +++ b/src/BizTalkSapEnvironmentInventory/Program.cs @@ -82,21 +82,36 @@ namespace BizTalkSapEnvironmentInventory logger.Info("Umgebung: " + options.EnvironmentName); logger.Info("Server: " + Environment.MachineName); 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); safeCollector.Execute( "System und BizTalk-Installation", true, - () => new SystemCollector(options).Collect(document)); + () => new SystemCollector( + options, + logger, + wmiTimeoutSeconds).Collect(document)); safeCollector.Execute( - "BizTalk-Anwendungen und WMI-Artefakte", + "BizTalk-Anwendungen und optionale WMI-Artefakte", true, () => new BizTalkWmiCollector( document, logger, - ReadIntSetting("WmiTimeoutSeconds", 30), - ReadIntSetting("MaxArtifactDetailsPerType", 5000)).Collect()); + wmiTimeoutSeconds, + maxArtifactDetails).Collect()); safeCollector.Execute( "SAP-Bindingparameter", true, @@ -104,7 +119,7 @@ namespace BizTalkSapEnvironmentInventory options, document, logger, - ReadIntSetting("ProcessTimeoutSeconds", 120)).Collect()); + processTimeoutSeconds).Collect()); safeCollector.Execute( "SAP NCo, SNC und Zertifikatsmetadaten", false, @@ -125,11 +140,22 @@ namespace BizTalkSapEnvironmentInventory } catch (Exception exception) { - logger.Error("Word-Dokument konnte nicht erzeugt werden: " + exception); + logger.Exception( + "Word-Dokument konnte nicht erzeugt werden", + exception); return 2; } 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); return exitCode; } diff --git a/src/BizTalkSapEnvironmentInventory/Reporting/DocxReportWriter.cs b/src/BizTalkSapEnvironmentInventory/Reporting/DocxReportWriter.cs index 5d888bf..d550996 100644 --- a/src/BizTalkSapEnvironmentInventory/Reporting/DocxReportWriter.cs +++ b/src/BizTalkSapEnvironmentInventory/Reporting/DocxReportWriter.cs @@ -167,9 +167,10 @@ namespace BizTalkSapEnvironmentInventory.Reporting WriteHeading(writer, "5. Vollständige BizTalk-Anwendungsliste", 1); WriteParagraph( writer, - "Die Liste stammt primär aus MSBTS_Application. Artefaktzahlen werden aus den " - + "lokal verfügbaren BizTalk-WMI-Klassen gebildet; optionale WMI-Klassen können " - + "versionsabhängig fehlen und werden dann im Erfassungsstatus bzw. als Finding ausgewiesen.", + "Die vollständige Anwendungsliste stammt aus dem read-only BizTalk Explorer " + + "Object Model. Artefaktzahlen werden aus lokal verfügbaren, optionalen " + + "BizTalk-WMI-Klassen gebildet; fehlende Klassen werden im Log und als " + + "Finding ausgewiesen, ohne die Anwendungsliste zu verwerfen.", "Normal", false, null);