Initial commit: BizTalk SAP environment inventory

This commit is contained in:
2026-07-27 14:21:50 +02:00
commit 439427cc17
26 changed files with 5281 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
root = true
[*]
charset = utf-8
end_of_line = crlf
insert_final_newline = true
indent_style = space
indent_size = 4
[*.md]
end_of_line = lf
trim_trailing_whitespace = false
+27
View File
@@ -0,0 +1,27 @@
name: Build und Test
on:
push:
pull_request:
jobs:
build:
runs-on: windows
steps:
- name: Repository auschecken
uses: actions/checkout@v4
- name: Release bauen und Tests ausführen
shell: cmd
run: scripts\build-release.cmd
- name: Deployment-Ordner erzeugen
shell: cmd
run: scripts\package-release.cmd
- name: Deployment-Artefakt bereitstellen
uses: actions/upload-artifact@v4
with:
name: BizTalkSapEnvironmentInventory-deploy
path: artifacts\BizTalkSapEnvironmentInventory-deploy
+9
View File
@@ -0,0 +1,9 @@
bin/
obj/
artifacts/
.vs/
*.user
*.suo
*.log
*.docx
+25
View File
@@ -0,0 +1,25 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.31729.503
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkSapEnvironmentInventory", "src\BizTalkSapEnvironmentInventory\BizTalkSapEnvironmentInventory.csproj", "{43962556-3DD2-4846-96E2-76B4749F17F2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkSapEnvironmentInventory.Tests", "tests\BizTalkSapEnvironmentInventory.Tests\BizTalkSapEnvironmentInventory.Tests.csproj", "{9F9E3933-BB12-4F05-984D-8B86C2AB963D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{43962556-3DD2-4846-96E2-76B4749F17F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{43962556-3DD2-4846-96E2-76B4749F17F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{43962556-3DD2-4846-96E2-76B4749F17F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{43962556-3DD2-4846-96E2-76B4749F17F2}.Release|Any CPU.Build.0 = Release|Any CPU
{9F9E3933-BB12-4F05-984D-8B86C2AB963D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9F9E3933-BB12-4F05-984D-8B86C2AB963D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9F9E3933-BB12-4F05-984D-8B86C2AB963D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9F9E3933-BB12-4F05-984D-8B86C2AB963D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
+407
View File
@@ -0,0 +1,407 @@
# Technische Dokumentation: BEW BizTalk SAP Environment Inventory
## 1. Zielbild und Topologie
Das Tool erzeugt je Umgebung einen belastbaren Ist-Bericht der SAP-Adapterkonfiguration und der vollständigen BizTalk-Anwendungsliste.
```text
ACC
+-- 1 BizTalk Server 2020 <-- Tool lokal ausführen
+-- 1 SQL Server <-- BizTalk-Datenbanken
PROD
+-- 1 BizTalk Server 2020 <-- Tool lokal ausführen
+-- 1 SQL Server <-- BizTalk-Datenbanken
```
Es gibt in ACC und PROD jeweils keinen zweiten BizTalk-Knoten, der verglichen werden müsste. Die Berichte sind getrennt zu erzeugen und anschließend gegenüberzustellen.
Aufgabenbezug:
- SAP-Adapter-Verbindungsparameter dokumentieren
- vollständige BizTalk-Anwendungsliste statt einer unvollständigen bisherigen App-Liste erzeugen
- tatsächliche Zuordnung der SAP-Verbindungen zu BizTalk-Anwendungen herstellen
- ERP-/ISU-Receive- und Send-Hosts dokumentieren
- SNC-Konfiguration, Bibliotheken und Zertifikats-/PSE-Metadaten erfassen
- IDoc-Basistypen/Erweiterungen sowie WE20/WE21 als Nachweise adressieren
- DR-Abhängigkeit zur SAP-seitigen Umschaltung nach Frankfurt sichtbar machen
## 2. Architektur
```text
Administrative cmd.exe
|
+-- run-inventory.cmd ACC|PROD
|
+-- BizTalkSapEnvironmentInventory.exe
|
+-- SystemCollector
| +-- Windows WMI
| +-- BizTalk Registry
| +-- Management SQL/DB
|
+-- BizTalkWmiCollector
| +-- root\MicrosoftBizTalkServer
| +-- Anwendungen und Artefakte
| +-- Adapter, Hosts und Handler
| +-- SAP-Endpunkt-Fallback
|
+-- BindingExportCollector
| +-- BTSTask ExportBindings /GroupLevel
| +-- WCF-SAP und WCF-Custom/sapBinding
| +-- SAP-URI und CustomProps
| +-- Secret-Redaktion
|
+-- SapRuntimeCollector
| +-- Adapter Pack/NCo-Installationen
| +-- Runtime-Dateiversion und Architektur
| +-- SNC/PSE/Zertifikatsmetadaten
|
+-- DocxReportWriter
+-- Office Open XML
+-- 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`.
## 3. Datenquellen
### 3.1 System und Registry
Windows-Daten stammen aus `root\cimv2`. BizTalk-Installations- und Managementinformationen werden in beiden Registry-Ansichten unter folgenden Schlüsseln gesucht:
```text
HKLM\SOFTWARE\Microsoft\BizTalk Server\3.0
HKLM\SOFTWARE\Microsoft\BizTalk Server\3.0\Administration
```
Ermittelt werden unter anderem:
- Produktname, Version und Edition
- Installationspfad
- BizTalk Management SQL Server
- BizTalk Management Database
- Betriebssystem, Build und Prozessarchitektur
Kommandozeilenparameter überschreiben automatisch ermittelte Management-DB-Werte.
### 3.2 BizTalk-WMI
Primärer Namespace:
```text
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`
- `MSBTS_SendPort`
- `MSBTS_SendPortGroup`
- `MSBTS_ReceivePort`
- `MSBTS_ReceiveLocation`
- `MSBTS_Assembly`
- `MSBTS_Schema`
- `MSBTS_Map`
- `MSBTS_Pipeline`
- `MSBTS_AdapterSetting`
- `MSBTS_ReceiveHandler`
- `MSBTS_SendHandler2` bzw. `MSBTS_SendHandler`
- `MSBTS_HostSetting`
- `MSBTS_HostInstance`
Eine fehlende optionale Klasse erzeugt einen Hinweis, verhindert aber nicht die Anwendungsliste oder den Word-Bericht.
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
Das Tool verwendet:
```cmd
BTSTask.exe ExportBindings ^
/GroupLevel ^
/Destination:"<temporäre Datei>" ^
/Server:"<Management SQL Server>" ^
/Database:"<Management Database>"
```
`/GroupLevel` ist entscheidend: Ohne diesen Parameter würde `BTSTask` nur die Default-Anwendung exportieren und die geforderte vollständige Port-/App-Zuordnung wäre nicht gewährleistet.
Die temporäre XML-Datei wird:
1. in einem eindeutigen Temp-Pfad erzeugt,
2. mit deaktivierter DTD-/External-Entity-Auflösung gelesen,
3. ausschließlich in sichere Berichtsmodelle überführt,
4. im `finally`-Block gelöscht.
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
Für Diagnose- oder Berechtigungsfälle kann ein vorhandener Export verarbeitet werden:
```cmd
BizTalkSapEnvironmentInventory.exe ^
--environment ACC ^
--binding-file C:\Temp\ACC-GroupBindings.xml
```
Die Datei wird nicht verändert.
## 4. SAP-Endpunkterkennung
Ein Endpunkt wird als SAP-relevant erkannt, wenn mindestens eines zutrifft:
- Adaptername enthält `SAP`
- Adresse beginnt mit `sap://`
- Binding Type enthält `sapBinding`
- Custom Properties enthalten `Microsoft.Adapters.SAP`
Dadurch werden sowohl dedizierte `WCF-SAP`-Ports als auch `WCF-Custom`-Ports mit SAP Custom Binding erkannt.
## 5. SAP-Verbindungsparameter
### 5.1 SAP-URI
Beispiel:
```text
sap://Client=100;lang=DE@A/sap.example.local/00
?GwHost=gateway.example.local
&GwServ=sapgw00
&ListenerProgramId=BIZTALK_ACC
```
Ausgewertete Parameter:
| Gruppe | Parameter |
| --- | --- |
| Login | `Client`, `Language` |
| Application Server | Host, System Number |
| Message Server | Host, R/3 System Name, Gruppe |
| Destination | Destination Name |
| Gateway | `GwHost`, `GwServ` |
| RFC Listener | `ListenerDest`, `ListenerGwHost`, `ListenerGwServ`, `ListenerProgramId` |
| Routing | `SAPROUTER` |
| Security | `UseSnc` |
### 5.2 Binding Properties
`TransportTypeData` und `ReceiveLocationTransportTypeData` enthalten bei WCF-Adaptern verschachtelte `CustomProps` und teilweise nochmals XML-kodierte Bindingkonfiguration. Der Parser verarbeitet diese Ebenen begrenzt rekursiv und erfasst unter anderem:
- `BindingType`
- `UseSnc`
- `SncLibrary`
- `SncPartnerName`
- `SncMyName`
- `SncQop`
- `AffiliateApplicationName`
- `UserName`
- `Action` und `ActionMapping`
Große Werte werden im Bericht begrenzt. Sensitive Werte werden vor Aufnahme in das Modell redigiert.
### 5.3 Anwendungszuordnung
Die Zuordnung erfolgt in dieser Reihenfolge:
1. `ApplicationName` im Gruppenbinding
2. gleichnamiger Send Port oder Receive Port aus WMI
3. keine Schätzung; stattdessen Finding
Damit wird sichtbar, welche der vollständigen BizTalk-Anwendungen tatsächlich SAP-Ports verwendet.
## 6. SAP NCo und Prozessarchitektur
Gesucht werden:
- installierter Microsoft BizTalk Adapter Pack/WCF LOB Adapter SDK
- SAP .NET Connector
- `sapnco.dll`
- `sapnco_utils.dll`
- SAP Cryptographic Library
- `sapgenpse.exe`
Für gefundene PE-Dateien werden Dateiversion, Produktversion, Änderungszeit und PE-Architektur dokumentiert. Dies unterstützt die Prüfung, ob der jeweilige 32-/64-Bit-BizTalk-Host zur installierten SAP-Laufzeit passt.
Die Erfassung durchsucht nur klar begrenzte Installations- und Konfigurationspfade bis zu einer Maximalebene. Es findet keine unbeschränkte Volltextsuche über alle Laufwerke statt.
## 7. SNC und Credential-Sicherheit
Der Security-Modus wird aus den Bindingparametern klassifiziert:
1. `UseSnc=true` → SAP SNC
2. SSO Affiliate Application vorhanden → Enterprise SSO
3. Benutzername vorhanden → SAP-Benutzer
4. sonst nicht eindeutig ableitbar
Dokumentiert werden dürfen:
- SAP-Benutzername oder SSO-Referenz
- SNC-Partner-/My-Name
- SNC-Bibliothek und QOP
- PSE-/Zertifikatspfad
- Fingerprint und Gültigkeit
- Vorhandensein eines privaten Schlüssels bei X.509-Zertifikaten
Nicht dokumentiert werden:
- Kennwörter
- PSE-Inhalte
- private Schlüssel
- Tokens
- entschlüsselbare Connection Strings
Rotation und Owner sind keine technischen Bindingeigenschaften. Sie bleiben als manueller Security-Nachweis offen.
## 8. IDoc, WE20 und WE21
### 8.1 Automatisch belegbare Indikatoren
Das Tool sucht nach SAP-/IDoc-Indikatoren in:
- Schema- und Assemblynamen
- Target Namespaces und Root Names
- WCF Actions
- SAP-Adapteroperationen
Dies kann IDoc-Basistypen wie beispielsweise `ORDERS05` sichtbar machen, ist aber kein Ersatz für eine SAP-seitige Bestandsaufnahme.
### 8.2 SAP-seitige Pflichtnachweise
Von SAP Basis beziehungsweise dem Fachteam beizustellen:
| Bereich | Benötigte Daten |
| --- | --- |
| WE20 | Partner, Nachrichtentyp, Basistyp, Erweiterung, Empfängerport, Verarbeitungsoptionen |
| WE21 | Portname, RFC-Destination, IDoc-Version, WE20-Zuordnung |
| RFC Destination | Typ, Zielsystem, Gateway, Program ID, Verbindungstest |
| IDoc | vollständige Liste der Basistypen und kundeneigenen Erweiterungen |
Das Tool markiert diese Punkte in jedem DOCX als offen, selbst wenn einzelne Namen aus dem BizTalk-Binding abgeleitet werden konnten.
## 9. Disaster Recovery
Der lokale BizTalk-Bericht zeigt die aktuell konfigurierten RFC-/Gateway-/Listenerwerte. Er kann nicht entscheiden, ob und wie die RFC-Destination im SAP-System auf Frankfurt umgeschaltet wird.
Das DR-Runbook muss mindestens enthalten:
1. Owner und Freigabe für die Umschaltung
2. betroffene RFC-Destinationen
3. Zielwerte in Frankfurt
4. SNC-PSE/Bibliothek und Identität am DR-Ziel
5. Reihenfolge BizTalk/SAP
6. Verbindungstest
7. Rückschaltung
8. Dokumentation des letzten Tests
## 10. Resilienz
- Collector-Isolation mit eigenem Status und Zeitmessung
- WMI-Timeout
- Prozess-Timeout für `BTSTask`
- WMI-Fallback für SAP-Endpunkte
- Offline-Bindingmodus
- optionale WMI-Klassen stoppen den Lauf nicht
- begrenzte Dateisystemsuche
- Fehlerisolation bei Registry, Zertifikaten und Dateimetadaten
- XML-Parser ohne DTD/External Entities
- atomare DOCX-Ausgabe
- eindeutige Dateinamen mit Umgebung, Server und Zeitstempel
- Exitcode `1` bei unvollständigem Pflichtabschnitt
## 11. DOCX-Erzeugung ohne Office
Der Report Writer erzeugt ein standardkonformes Office-Open-XML-Paket mit:
```text
[Content_Types].xml
_rels/.rels
docProps/core.xml
docProps/app.xml
word/document.xml
word/styles.xml
word/_rels/document.xml.rels
```
Alle Texte werden über `XmlWriter` geschrieben. Dadurch werden Host-, Anwendungs- und Portnamen als Text behandelt und können kein XML-Markup einschleusen.
## 12. Lauf auf ACC und PROD
Auf jedem der beiden BizTalk Server:
1. Deployment in einen lokalen Ordner kopieren.
2. Administrative `cmd.exe` öffnen.
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.
ACC:
```cmd
run-inventory.cmd ACC C:\BizTalk-Doku\ACC
```
PROD:
```cmd
run-inventory.cmd PROD C:\BizTalk-Doku\PROD
```
## 13. Build und Tests
Buildbaseline:
- klassisches MSBuild-Projekt
- .NET Framework 4.7.2
- C# 7.3
- Visual Studio 2019/MSBuild 16.x
- keine SDK-style-Projekte
- keine NuGet-Pakete
```cmd
scripts\build-release.cmd
scripts\package-release.cmd
```
Tests prüfen:
- Secret-Redaktion in XML und SAP-URI
- Parsing eines WCF-SAP-Gruppenbindings
- SAP-Client, Host und `UseSnc`
- Anwendungszuordnung
- DOCX-Paketstruktur
- XML-Gültigkeit aller DOCX-Parts
- Ausschluss von Testkennwörtern
## 14. Bekannte Grenzen
- kein SAP-Login
- kein aktiver RFC-Ping
- keine direkte Abfrage von WE20, WE21 oder SM59
- keine Auflösung von Credential-Owner oder Rotation
- 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
- 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 WMI Technical Reference
- Microsoft: WCF-SAP Port Configuration
- Microsoft: SAP System Connection URI
- Microsoft: Security between the SAP system and the adapter
+231
View File
@@ -0,0 +1,231 @@
# BEW BizTalk SAP Environment Inventory
`BEW BizTalk SAP Environment Inventory` erzeugt lokal auf einem BizTalk Server 2020 eine Microsoft-Word-Dokumentation (`.docx`) der SAP-Adapterverbindungen und eine vollständige Liste aller BizTalk-Anwendungen.
Das Tool wird einmal auf dem BizTalk Server in `ACC` und einmal auf dem BizTalk Server in `PROD` ausgeführt. Beide Umgebungen bestehen jeweils aus:
- 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.
## Erfasster Umfang
### Vollständige BizTalk-Anwendungsliste
- alle Anwendungen aus `MSBTS_Application`
- Anwendungsstatus und Beschreibung
- Anzahl und Zuordnung von Orchestrierungen
- Send Ports und Send Port Groups
- Receive Ports und Receive Locations
- Assemblies, Schemas, Maps und Pipelines, soweit die jeweilige WMI-Klasse verfügbar ist
- SAP-/IDoc-Indikatoren aus bereitgestellten Schemas, Actions und Artefaktnamen
Die Liste ist nicht auf eine bisher bekannte 15-App-Liste begrenzt. Damit werden auch weitere Anwendungen wie beispielsweise `MasterData_SAP_WebGIS`, `MasterDataHeat`, `WoWHeatInvoice` oder `MeterChangeInfo/Order` sichtbar, sofern sie tatsächlich in der jeweiligen BizTalk-Gruppe installiert sind.
### SAP-Adapterverbindungen
Aus dem BizTalk-Gruppenbinding und WMI werden unter anderem dokumentiert:
- BizTalk-Anwendung und Portzuordnung
- Sende- oder Empfangsrichtung
- WCF-SAP bzw. WCF-Custom mit `sapBinding`
- Port-, Receive-Location- und Handlername
- RFC-/Listener-Destination
- SAP-System-ID beziehungsweise R/3-Systemname
- Application Server oder Message Server
- Systemnummer
- Gateway Host und Gateway Service
- SAP Client und Sprache
- Listener Program ID
- SAP Router
- Actions beziehungsweise erkannte RFC-/IDoc-Operationen
- SSO-/Benutzerreferenz, jedoch niemals das Kennwort
### SAP NCo, Hosts und SNC
- konfigurierte SAP-Adapterdefinitionen
- Receive-/Send-Handler und BizTalk Hosts
- bekannte ERP-/ISU-Hostfamilien werden nicht fest verdrahtet, sondern aus der realen Umgebung gelesen
- SAP .NET Connector und Adapter-Pack-Installationen
- `sapnco.dll`, `sapnco_utils.dll` und Dateiversion/Architektur
- `UseSnc`, `SncLibrary`, `SncPartnerName`, `SncMyName` und `SncQop`
- `SNC_LIB`, `SECUDIR` und `SAPNWRFC_HOME`
- PSE-/Zertifikatspfad, Gültigkeit und Fingerprint, soweit lokal lesbar
## Was nicht automatisch aus BizTalk ausgelesen werden kann
Folgende Informationen liegen im SAP-System oder in Betriebsprozessen und werden im Bericht als offene Nachweise ausgewiesen:
- Partnerprofile aus Transaktion `WE20`
- Portdefinitionen aus Transaktion `WE21`
- vollständige SAP-seitige RFC-Destination einschließlich Verbindungstest
- verbindliche Liste der IDoc-Basistypen und Erweiterungen
- tatsächliche SAP-Kennwörter
- Owner und Rotationsprozess für SAP-Credentials bzw. SNC-PSE/Zertifikate
- DR-Verfahren für die SAP-seitige Umschaltung der RFC-Destination nach Frankfurt
Das Tool erfindet hierfür keine Werte. Es dokumentiert nur lokal belegbare Indikatoren und nennt die erforderliche Ergänzung sowie die zuständige Rolle.
## Sicherheitsprinzip
- ausschließlich read-only WMI- und Registry-Abfragen
- `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
- zusätzliche Redigierung von Passwort-, Secret-, Token-, Private-Key- und Connection-String-Feldern
- keine privaten Schlüssel oder PSE-Inhalte im DOCX
- kein SAP-Login und kein aktiver RFC-Verbindungstest
DOCX und Log enthalten trotzdem interne Host-, Anwendungs-, Zertifikats- und Pfadinformationen und müssen geschützt abgelegt werden.
## Voraussetzungen auf ACC und PROD
- lokale Ausführung auf dem jeweiligen BizTalk Server
- administrative `cmd.exe`
- 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
- `BTSTask.exe` aus der lokalen BizTalk-Installation
## Schnellstart
1. Deployment-Ordner auf den BizTalk Server kopieren.
2. Administrative `cmd.exe` öffnen.
3. In den Deployment-Ordner wechseln.
4. Self-Test starten.
5. Inventarisierung für die richtige Umgebung ausführen.
Self-Test:
```cmd
BizTalkSapEnvironmentInventory.exe --self-test
```
ACC:
```cmd
run-inventory.cmd ACC C:\BizTalk-Doku\ACC
```
PROD:
```cmd
run-inventory.cmd PROD C:\BizTalk-Doku\PROD
```
Direkter Aufruf:
```cmd
BizTalkSapEnvironmentInventory.exe ^
--environment ACC ^
--output C:\BizTalk-Doku\ACC
```
## Konsolenausgabe
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] Exportiere BizTalk-Gruppenbindings read-only mit BTSTask.
[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\...
```
Fehler eines Collectors stoppen die übrigen Abschnitte nicht. Sie erscheinen im Log, im Erfassungsstatus und als Finding im Word-Bericht.
## Ausgabedateien
```text
BizTalk-SAP-Dokumentation-ACC-BIZTALKSERVER-20260727-150000.docx
BizTalk-SAP-Dokumentation-ACC-BIZTALKSERVER-20260727-150000.log
```
Das DOCX wird direkt als Office Open XML erzeugt. Auf dem BizTalk Server wird keine Office-Anwendung benötigt.
## Kommandozeilenoptionen
| Option | Bedeutung |
| --- | --- |
| `--environment NAME` | Umgebung, regulär `ACC` oder `PROD`. |
| `--output PFAD` | Zielordner für DOCX und Log. |
| `--management-server NAME` | SQL-Server der BizTalk Management Database; normalerweise aus Registry. |
| `--management-database NAME` | Name der Management Database; Standard/Fallback `BizTalkMgmtDb`. |
| `--btstask DATEI` | Expliziter Pfad zu `BTSTask.exe`. |
| `--binding-file DATEI` | Vorhandenen Binding-Export offline auswerten, ohne `BTSTask` aufzurufen. |
| `--self-test` | Prüft Secret-Redaktion, SAP-Parser und DOCX-Struktur. |
| `--help` | Hilfe anzeigen. |
## Exitcodes
| Code | Bedeutung |
| ---: | --- |
| `0` | Pflichtabschnitte vollständig und DOCX erzeugt. |
| `1` | DOCX erzeugt, aber mindestens ein Pflichtabschnitt war unvollständig. |
| `2` | Fataler Aufruf-, Ausgabe- oder Berichtserzeugungsfehler. |
## Build für BizTalk Server 2020
Die Solution verwendet bewusst das klassische Visual-Studio-2019-Projektformat:
| Komponente | Vorgabe |
| --- | --- |
| Ziel | .NET Framework 4.7.2 |
| Sprache | C# 7.3 |
| Projektformat | klassisches MSBuild, kein `Microsoft.NET.Sdk` |
| Buildumgebung | Visual Studio 2019/Build Tools, MSBuild 16.x oder neuer |
| Pakete | keine NuGet-Abhängigkeiten |
Build und Tests:
```cmd
scripts\build-release.cmd
scripts\package-release.cmd
```
Deployment:
```text
artifacts\BizTalkSapEnvironmentInventory-deploy\
```
Die Gitea-Workflowdatei `.gitea/workflows/build.yml` baut, testet und veröffentlicht den Deployment-Ordner auf einem Windows-Runner.
## Troubleshooting
### Keine Anwendungen
- lokal auf dem BizTalk Server ausführen
- Konto und Mitgliedschaft in den BizTalk-Administrator-/Operator-Gruppen prüfen
- WMI-Namespace `root\MicrosoftBizTalkServer` prüfen
### BTSTask findet die Management Database nicht
Management SQL Server und Datenbank explizit angeben:
```cmd
BizTalkSapEnvironmentInventory.exe ^
--environment ACC ^
--management-server SQL-ACC ^
--management-database BizTalkMgmtDb
```
Alternativ einen autorisierten Binding-Export bereitstellen:
```cmd
BizTalkSapEnvironmentInventory.exe ^
--environment ACC ^
--binding-file C:\Temp\ACC-GroupBindings.xml
```
### Keine SAP-Endpunkte
- prüfen, ob der Adapter als `WCF-SAP` oder als `WCF-Custom` mit `sapBinding` verwendet wird
- vollständigen Gruppenbindingexport statt nur einer Anwendung verwenden
- DOCX-Findings und Log prüfen
Weitere Architektur- und Sicherheitsdetails stehen in [Dokumentation.md](Dokumentation.md).
+52
View File
@@ -0,0 +1,52 @@
@echo off
setlocal EnableExtensions
set "APPDIR=%~dp0"
set "EXE=%APPDIR%BizTalkSapEnvironmentInventory.exe"
if not exist "%EXE%" (
echo FEHLER: Anwendung fehlt: "%EXE%"
exit /b 2
)
if "%~1"=="" goto :usage
set "ENVIRONMENT=%~1"
if "%~2"=="" (
set "OUTPUT=%CD%\BizTalk-SAP-Dokumentation\%ENVIRONMENT%"
) else (
set "OUTPUT=%~2"
)
echo ============================================================
echo BEW BizTalk SAP Environment Inventory
echo Umgebung: %ENVIRONMENT%
echo Server: %COMPUTERNAME%
echo Ausgabe: %OUTPUT%
echo Modus: read-only
echo ============================================================
echo.
"%EXE%" --environment "%ENVIRONMENT%" --output "%OUTPUT%"
set "EXITCODE=%ERRORLEVEL%"
echo.
if "%EXITCODE%"=="0" (
echo Erfassung vollstaendig abgeschlossen.
) else if "%EXITCODE%"=="1" (
echo Erfassung mit Teilfehlern abgeschlossen. DOCX und Log pruefen.
) else (
echo Erfassung konnte nicht abgeschlossen werden.
)
echo ExitCode: %EXITCODE%
exit /b %EXITCODE%
:usage
echo Aufruf:
echo run-inventory.cmd UMGEBUNG [AUSGABEORDNER]
echo.
echo Beispiele:
echo run-inventory.cmd ACC C:\BizTalk-Doku\ACC
echo run-inventory.cmd PROD C:\BizTalk-Doku\PROD
exit /b 2
+44
View File
@@ -0,0 +1,44 @@
@echo off
setlocal EnableExtensions
set "ROOT=%~dp0.."
set "SOLUTION=%ROOT%\BizTalkSapEnvironmentInventory.sln"
if not defined MSBUILD (
set "VSWHERE=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe"
if exist "%VSWHERE%" (
for /f "usebackq tokens=*" %%i in (`"%VSWHERE%" -latest -requires Microsoft.Component.MSBuild -find MSBuild\**\Bin\MSBuild.exe`) do (
set "MSBUILD=%%i"
goto :found_msbuild
)
)
)
:found_msbuild
if not defined MSBUILD (
for /f "tokens=*" %%i in ('where msbuild 2^>nul') do (
set "MSBUILD=%%i"
goto :found_msbuild_path
)
)
:found_msbuild_path
if not defined MSBUILD (
echo FEHLER: MSBuild fehlt. Visual Studio 2019 Build Tools und das .NET Framework 4.7.2 Developer Pack installieren.
exit /b 2
)
echo Verwende MSBuild: %MSBUILD%
"%MSBUILD%" -version
echo Baue klassisches .NET-Framework-4.7.2-Projekt ohne .NET-SDK- oder NuGet-Abhaengigkeit ...
"%MSBUILD%" "%SOLUTION%" /m /t:Rebuild /p:Configuration=Release /p:Platform="Any CPU"
if errorlevel 1 exit /b %ERRORLEVEL%
echo Fuehre Tests aus ...
"%ROOT%\tests\BizTalkSapEnvironmentInventory.Tests\bin\Release\BizTalkSapEnvironmentInventory.Tests.exe"
if errorlevel 1 exit /b %ERRORLEVEL%
echo Fuehre Self-Test der Anwendung aus ...
"%ROOT%\src\BizTalkSapEnvironmentInventory\bin\Release\BizTalkSapEnvironmentInventory.exe" --self-test
exit /b %ERRORLEVEL%
+25
View File
@@ -0,0 +1,25 @@
@echo off
setlocal EnableExtensions
set "ROOT=%~dp0.."
set "BIN=%ROOT%\src\BizTalkSapEnvironmentInventory\bin\Release"
set "ARTIFACTS=%ROOT%\artifacts"
set "DEPLOY=%ARTIFACTS%\BizTalkSapEnvironmentInventory-deploy"
call "%ROOT%\scripts\build-release.cmd"
if errorlevel 1 exit /b %ERRORLEVEL%
if exist "%DEPLOY%" rmdir /s /q "%DEPLOY%"
mkdir "%DEPLOY%"
copy "%BIN%\BizTalkSapEnvironmentInventory.exe" "%DEPLOY%\" >nul
copy "%BIN%\BizTalkSapEnvironmentInventory.exe.config" "%DEPLOY%\" >nul
if exist "%BIN%\BizTalkSapEnvironmentInventory.pdb" copy "%BIN%\BizTalkSapEnvironmentInventory.pdb" "%DEPLOY%\" >nul
copy "%ROOT%\deployment\run-inventory.cmd" "%DEPLOY%\" >nul
copy "%ROOT%\Readme.md" "%DEPLOY%\" >nul
copy "%ROOT%\Dokumentation.md" "%DEPLOY%\" >nul
echo Deployment-Ordner erstellt:
echo %DEPLOY%
exit /b 0
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<appSettings>
<add key="WmiTimeoutSeconds" value="30" />
<add key="ProcessTimeoutSeconds" value="120" />
<add key="ExpectedUseSnc" value="true" />
<add key="IncludeSapRelatedLocalMachineCertificates" value="true" />
<add key="MaxArtifactDetailsPerType" value="5000" />
</appSettings>
<startup useLegacyV2RuntimeActivationPolicy="true">
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
</configuration>
@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{43962556-3DD2-4846-96E2-76B4749F17F2}</ProjectGuid>
<OutputType>Exe</OutputType>
<RootNamespace>BizTalkSapEnvironmentInventory</RootNamespace>
<AssemblyName>BizTalkSapEnvironmentInventory</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<TargetFrameworkProfile />
<FileAlignment>512</FileAlignment>
<LangVersion>7.3</LangVersion>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<PlatformTarget>AnyCPU</PlatformTarget>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<PlatformTarget>AnyCPU</PlatformTarget>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Configuration" />
<Reference Include="System.Core" />
<Reference Include="System.IO.Compression" />
<Reference Include="System.IO.Compression.FileSystem" />
<Reference Include="System.Management" />
<Reference Include="System.Security" />
<Reference Include="System.Xml" />
<Reference Include="System.Xml.Linq" />
</ItemGroup>
<ItemGroup>
<Compile Include="Collectors\BindingExportCollector.cs" />
<Compile Include="Collectors\BizTalkWmiCollector.cs" />
<Compile Include="Collectors\SapRuntimeCollector.cs" />
<Compile Include="Collectors\SystemCollector.cs" />
<Compile Include="Configuration\CommandLineOptions.cs" />
<Compile Include="Infrastructure\ConsoleFileLogger.cs" />
<Compile Include="Infrastructure\SafeCollector.cs" />
<Compile Include="Infrastructure\SelfTestRunner.cs" />
<Compile Include="Infrastructure\SensitiveDataSanitizer.cs" />
<Compile Include="Models\InventoryModels.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Reporting\DocxReportWriter.cs" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
@@ -0,0 +1,796 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Linq;
using BizTalkSapEnvironmentInventory.Configuration;
using BizTalkSapEnvironmentInventory.Infrastructure;
using BizTalkSapEnvironmentInventory.Models;
namespace BizTalkSapEnvironmentInventory.Collectors
{
/// <summary>
/// Exportiert BizTalk-Gruppenbindings read-only und extrahiert sichere SAP-Endpunktparameter.
/// </summary>
internal sealed class BindingExportCollector
{
private static readonly Regex ApplicationModuleName = new Regex(
@"^\[Application:(.+)\]$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private readonly CommandLineOptions options;
private readonly InventoryDocument document;
private readonly ConsoleFileLogger logger;
private readonly TimeSpan processTimeout;
public BindingExportCollector(
CommandLineOptions options,
InventoryDocument document,
ConsoleFileLogger logger,
int timeoutSeconds)
{
this.options = options;
this.document = document;
this.logger = logger;
processTimeout = TimeSpan.FromSeconds(Math.Max(30, timeoutSeconds));
}
/// <summary>
/// Verarbeitet entweder eine vorgegebene Binding-Datei oder einen temporären BTSTask-Export.
/// </summary>
public void Collect()
{
if (!string.IsNullOrWhiteSpace(options.BindingFilePath))
{
logger.Info("Werte vorhandenen Binding-Export offline aus: " + options.BindingFilePath);
ParseBindingDocument(LoadXml(options.BindingFilePath), "Angegebene Binding-Datei");
return;
}
var btsTask = ResolveBtsTaskPath();
if (string.IsNullOrWhiteSpace(btsTask))
{
throw new FileNotFoundException(
"BTSTask.exe wurde nicht gefunden. --btstask oder --binding-file verwenden.");
}
var temporaryPath = Path.Combine(
Path.GetTempPath(),
"BizTalkSapInventory-" + Guid.NewGuid().ToString("N") + ".xml");
try
{
ExportBindings(btsTask, temporaryPath);
ParseBindingDocument(LoadXml(temporaryPath), "BTSTask GroupLevel Export");
}
finally
{
TryDelete(temporaryPath);
}
}
/// <summary>
/// Parst ein Binding-Dokument; intern sichtbar, damit der Self-Test reale XML-Strukturen prüft.
/// </summary>
internal void ParseBindingDocument(XDocument bindingDocument, string source)
{
if (bindingDocument == null || bindingDocument.Root == null)
{
throw new InvalidDataException("Binding-XML ist leer.");
}
AddApplicationsFromBinding(bindingDocument);
var parsed = 0;
foreach (var sendPort in bindingDocument.Descendants()
.Where(item => IsName(item, "SendPort") && HasAncestor(item, "SendPortCollection")))
{
var endpoint = ParseSendPort(sendPort, source);
if (endpoint != null)
{
MergeEndpoint(endpoint);
parsed++;
}
}
foreach (var receiveLocation in bindingDocument.Descendants()
.Where(item => IsName(item, "ReceiveLocation")
&& HasAncestor(item, "ReceiveLocations")))
{
var endpoint = ParseReceiveLocation(receiveLocation, source);
if (endpoint != null)
{
MergeEndpoint(endpoint);
parsed++;
}
}
logger.Info(parsed + " SAP-Endpunkt(e) aus dem Binding-Export ausgewertet.");
}
private SapEndpointRecord ParseSendPort(XElement sendPort, string source)
{
var primaryTransport = Child(sendPort, "PrimaryTransport");
var transportType = primaryTransport == null ? null : Child(primaryTransport, "TransportType");
var transportData = primaryTransport == null ? null : Child(primaryTransport, "TransportTypeData");
var properties = ParsePropertyBag(transportData);
var adapterName = Attribute(transportType, "Name");
var address = ChildValue(primaryTransport, "Address");
if (!LooksLikeSap(adapterName, address, properties))
{
return null;
}
var endpoint = new SapEndpointRecord
{
ApplicationName = FirstNonEmpty(
Attribute(sendPort, "ApplicationName"),
ChildValue(sendPort, "ApplicationName")),
Direction = "Senden",
Name = Attribute(sendPort, "Name"),
ParentPortName = Attribute(sendPort, "Name"),
AdapterName = adapterName,
HostName = FirstNonEmpty(
Attribute(Child(primaryTransport, "SendHandler"), "Name"),
ChildValue(primaryTransport, "SendHandler")),
Status = FirstNonEmpty(Attribute(sendPort, "Status"), "Konfiguriert"),
Address = SensitiveDataSanitizer.SanitizeText(address),
Source = source
};
AddProperties(endpoint, properties);
ParseSapAddress(endpoint, address);
FinalizeEndpoint(endpoint);
return endpoint;
}
private SapEndpointRecord ParseReceiveLocation(XElement receiveLocation, string source)
{
var transportType = Child(receiveLocation, "ReceiveLocationTransportType");
var transportData = Child(receiveLocation, "ReceiveLocationTransportTypeData");
var properties = ParsePropertyBag(transportData);
var adapterName = Attribute(transportType, "Name");
var address = FirstNonEmpty(
ChildValue(receiveLocation, "Address"),
Attribute(receiveLocation, "Address"));
if (!LooksLikeSap(adapterName, address, properties))
{
return null;
}
var receivePort = receiveLocation.Ancestors()
.FirstOrDefault(item => IsName(item, "ReceivePort"));
var endpoint = new SapEndpointRecord
{
ApplicationName = FirstNonEmpty(
Attribute(receiveLocation, "ApplicationName"),
ChildValue(receiveLocation, "ApplicationName"),
Attribute(receivePort, "ApplicationName"),
ChildValue(receivePort, "ApplicationName")),
Direction = "Empfangen",
Name = Attribute(receiveLocation, "Name"),
ParentPortName = Attribute(receivePort, "Name"),
AdapterName = adapterName,
HostName = FirstNonEmpty(
Attribute(Child(receiveLocation, "ReceiveHandler"), "Name"),
ChildValue(receiveLocation, "ReceiveHandler")),
Status = FirstNonEmpty(
Attribute(receiveLocation, "Enable"),
Attribute(receiveLocation, "Status"),
"Konfiguriert"),
Address = SensitiveDataSanitizer.SanitizeText(address),
Source = source
};
AddProperties(endpoint, properties);
ParseSapAddress(endpoint, address);
FinalizeEndpoint(endpoint);
return endpoint;
}
private static List<NameValueRecord> ParsePropertyBag(XElement dataElement)
{
var result = new List<NameValueRecord>();
if (dataElement == null)
{
return result;
}
var raw = dataElement.HasElements
? string.Concat(dataElement.Nodes().Select(item => item.ToString(SaveOptions.DisableFormatting)))
: dataElement.Value;
ParseXmlProperties(raw, "Binding", result, 0);
return result;
}
private static void ParseXmlProperties(
string raw,
string source,
List<NameValueRecord> target,
int depth)
{
if (string.IsNullOrWhiteSpace(raw) || depth > 4)
{
return;
}
raw = WebUtility.HtmlDecode(raw.Trim());
if (!raw.StartsWith("<", StringComparison.Ordinal))
{
return;
}
XElement root;
try
{
root = XElement.Parse(raw, LoadOptions.None);
}
catch (XmlException)
{
return;
}
foreach (var element in root.DescendantsAndSelf())
{
foreach (var attribute in element.Attributes())
{
if (string.Equals(attribute.Name.LocalName, "vt", StringComparison.OrdinalIgnoreCase)
|| attribute.IsNamespaceDeclaration)
{
continue;
}
AddUnique(
target,
element.Name.LocalName + "." + attribute.Name.LocalName,
SensitiveDataSanitizer.RedactValue(attribute.Name.LocalName, attribute.Value),
source);
AddUnique(
target,
NormalizePropertyName(attribute.Name.LocalName),
SensitiveDataSanitizer.RedactValue(attribute.Name.LocalName, attribute.Value),
source);
}
if (element.HasElements)
{
continue;
}
var name = element.Name.LocalName;
if (!SensitiveDataSanitizer.IsSensitiveName(name)
&& element.Value.TrimStart().StartsWith("<", StringComparison.Ordinal))
{
ParseXmlProperties(element.Value, name, target, depth + 1);
continue;
}
var value = SensitiveDataSanitizer.RedactValue(name, element.Value);
AddUnique(target, name, value, source);
}
}
private static void ParseSapAddress(SapEndpointRecord endpoint, string rawAddress)
{
if (string.IsNullOrWhiteSpace(rawAddress)
|| !rawAddress.StartsWith("sap://", StringComparison.OrdinalIgnoreCase))
{
return;
}
var address = SensitiveDataSanitizer.SanitizeText(rawAddress);
var withoutScheme = address.Substring("sap://".Length);
var atIndex = withoutScheme.IndexOf('@');
var userInfo = atIndex >= 0 ? withoutScheme.Substring(0, atIndex) : string.Empty;
var hostAndQuery = atIndex >= 0 ? withoutScheme.Substring(atIndex + 1) : withoutScheme;
foreach (var item in userInfo.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries))
{
var pair = item.Split(new[] { '=' }, 2);
if (pair.Length == 2)
{
AddUnique(
endpoint.Properties,
NormalizePropertyName(pair[0]),
Decode(pair[1]),
"SAP-URI");
}
}
var queryIndex = hostAndQuery.IndexOf('?');
var hostPart = queryIndex >= 0
? hostAndQuery.Substring(0, queryIndex)
: hostAndQuery;
var query = queryIndex >= 0
? hostAndQuery.Substring(queryIndex + 1)
: string.Empty;
var hostSegments = hostPart.Split('/');
if (hostSegments.Length > 0)
{
AddUnique(endpoint.Properties, "ConnectionType", Decode(hostSegments[0]), "SAP-URI");
if (string.Equals(hostSegments[0], "A", StringComparison.OrdinalIgnoreCase))
{
if (hostSegments.Length > 1)
{
AddUnique(endpoint.Properties, "ApplicationServerHost", Decode(hostSegments[1]), "SAP-URI");
}
if (hostSegments.Length > 2)
{
AddUnique(endpoint.Properties, "SystemNumber", Decode(hostSegments[2]), "SAP-URI");
}
}
else if (string.Equals(hostSegments[0], "B", StringComparison.OrdinalIgnoreCase))
{
if (hostSegments.Length > 1)
{
AddUnique(endpoint.Properties, "MessageServerHost", Decode(hostSegments[1]), "SAP-URI");
}
if (hostSegments.Length > 2)
{
AddUnique(endpoint.Properties, "R3SystemName", Decode(hostSegments[2]), "SAP-URI");
}
}
else if (string.Equals(hostSegments[0], "D", StringComparison.OrdinalIgnoreCase)
&& hostSegments.Length > 1)
{
AddUnique(endpoint.Properties, "DestinationName", Decode(hostSegments[1]), "SAP-URI");
}
}
foreach (var item in query.Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries))
{
var pair = item.Split(new[] { '=' }, 2);
if (pair.Length == 2)
{
AddUnique(
endpoint.Properties,
NormalizePropertyName(Decode(pair[0])),
SensitiveDataSanitizer.RedactValue(pair[0], Decode(pair[1])),
"SAP-URI");
}
}
}
private void FinalizeEndpoint(SapEndpointRecord endpoint)
{
ResolveApplication(endpoint);
var useSnc = endpoint.GetProperty("UseSnc", "UseSNC");
var sncEnabled = IsTrue(useSnc);
var affiliateApplication = endpoint.GetProperty(
"AffiliateApplicationName",
"SsoAffiliateApplication",
"SSOApplication");
var userName = endpoint.GetProperty("UserName", "Username");
if (sncEnabled)
{
endpoint.SecurityMode = "SAP SNC";
}
else if (!string.IsNullOrWhiteSpace(affiliateApplication))
{
endpoint.SecurityMode = "Enterprise SSO";
}
else if (!string.IsNullOrWhiteSpace(userName))
{
endpoint.SecurityMode = "SAP-Benutzer";
}
else
{
endpoint.SecurityMode = "Nicht eindeutig aus Binding ableitbar";
}
if (!string.IsNullOrWhiteSpace(affiliateApplication))
{
endpoint.CredentialReference = "SSO Affiliate Application: " + affiliateApplication;
}
else if (!string.IsNullOrWhiteSpace(userName))
{
endpoint.CredentialReference = "SAP-Benutzer: " + userName
+ "; Kennwort wird nicht exportiert/dokumentiert.";
}
else
{
endpoint.CredentialReference =
"Keine Kennwortinformation im Binding-Export; Credentials separat kontrollieren.";
}
foreach (var property in endpoint.Properties.Where(item =>
item.Name.IndexOf("Action", StringComparison.OrdinalIgnoreCase) >= 0
|| item.Value.IndexOf("Microsoft.LobServices.Sap", StringComparison.OrdinalIgnoreCase) >= 0))
{
var operation = SensitiveDataSanitizer.SanitizeText(property.Value);
if (!string.IsNullOrWhiteSpace(operation)
&& !endpoint.Operations.Contains(operation, StringComparer.OrdinalIgnoreCase))
{
endpoint.Operations.Add(operation);
}
}
}
private void ResolveApplication(SapEndpointRecord endpoint)
{
if (!string.IsNullOrWhiteSpace(endpoint.ApplicationName))
{
EnsureApplication(endpoint.ApplicationName);
return;
}
foreach (var application in document.Applications)
{
if (application.Artifacts.Any(item =>
string.Equals(item.Name, endpoint.Name, StringComparison.OrdinalIgnoreCase)
|| (!string.IsNullOrWhiteSpace(endpoint.ParentPortName)
&& string.Equals(
item.Name,
endpoint.ParentPortName,
StringComparison.OrdinalIgnoreCase))))
{
endpoint.ApplicationName = application.Name;
return;
}
}
}
private void MergeEndpoint(SapEndpointRecord parsed)
{
var existing = document.SapEndpoints.FirstOrDefault(item =>
string.Equals(item.Direction, parsed.Direction, StringComparison.OrdinalIgnoreCase)
&& string.Equals(item.Name, parsed.Name, StringComparison.OrdinalIgnoreCase));
if (existing == null)
{
document.SapEndpoints.Add(parsed);
return;
}
existing.ApplicationName = FirstNonEmpty(parsed.ApplicationName, existing.ApplicationName);
existing.ParentPortName = FirstNonEmpty(parsed.ParentPortName, existing.ParentPortName);
existing.AdapterName = FirstNonEmpty(parsed.AdapterName, existing.AdapterName);
existing.HostName = FirstNonEmpty(parsed.HostName, existing.HostName);
existing.Status = FirstNonEmpty(parsed.Status, existing.Status);
existing.Address = FirstNonEmpty(parsed.Address, existing.Address);
existing.SecurityMode = FirstNonEmpty(parsed.SecurityMode, existing.SecurityMode);
existing.CredentialReference = FirstNonEmpty(
parsed.CredentialReference,
existing.CredentialReference);
existing.Source = FirstNonEmpty(parsed.Source, existing.Source);
foreach (var property in parsed.Properties)
{
AddUnique(existing.Properties, property.Name, property.Value, property.Source);
}
foreach (var operation in parsed.Operations)
{
if (!existing.Operations.Contains(operation, StringComparer.OrdinalIgnoreCase))
{
existing.Operations.Add(operation);
}
}
}
private void AddApplicationsFromBinding(XDocument bindingDocument)
{
foreach (var module in bindingDocument.Descendants().Where(item => IsName(item, "ModuleRef")))
{
var match = ApplicationModuleName.Match(Attribute(module, "Name"));
if (match.Success)
{
EnsureApplication(match.Groups[1].Value);
}
}
foreach (var applicationName in bindingDocument.Descendants()
.Where(item => IsName(item, "ApplicationName"))
.Select(item => item.Value)
.Where(item => !string.IsNullOrWhiteSpace(item))
.Distinct(StringComparer.OrdinalIgnoreCase))
{
EnsureApplication(applicationName);
}
}
private void EnsureApplication(string name)
{
if (string.IsNullOrWhiteSpace(name)
|| document.Applications.Any(item =>
string.Equals(item.Name, name, StringComparison.OrdinalIgnoreCase)))
{
return;
}
document.Applications.Add(new ApplicationRecord
{
Name = name.Trim(),
Status = "Aus Binding-Export erkannt"
});
}
private void ExportBindings(string btsTaskPath, string destination)
{
var arguments = new StringBuilder();
arguments.Append("ExportBindings /GroupLevel /Destination:\"")
.Append(EscapeCommandArgument(destination))
.Append("\"");
if (!string.IsNullOrWhiteSpace(document.System.ManagementServer))
{
arguments.Append(" /Server:\"")
.Append(EscapeCommandArgument(document.System.ManagementServer))
.Append("\"");
}
if (!string.IsNullOrWhiteSpace(document.System.ManagementDatabase))
{
arguments.Append(" /Database:\"")
.Append(EscapeCommandArgument(document.System.ManagementDatabase))
.Append("\"");
}
logger.Info("Exportiere BizTalk-Gruppenbindings read-only mit BTSTask.");
var output = new StringBuilder();
var startInfo = new ProcessStartInfo
{
FileName = btsTaskPath,
Arguments = arguments.ToString(),
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
WorkingDirectory = Path.GetDirectoryName(btsTaskPath)
};
using (var process = new Process { StartInfo = startInfo })
{
process.OutputDataReceived += (sender, args) =>
{
if (!string.IsNullOrWhiteSpace(args.Data))
{
output.AppendLine(args.Data);
}
};
process.ErrorDataReceived += (sender, args) =>
{
if (!string.IsNullOrWhiteSpace(args.Data))
{
output.AppendLine(args.Data);
}
};
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
if (!process.WaitForExit((int)processTimeout.TotalMilliseconds))
{
try
{
process.Kill();
}
catch (InvalidOperationException)
{
}
throw new TimeoutException(
"BTSTask ExportBindings überschritt " + processTimeout.TotalSeconds + " Sekunden.");
}
process.WaitForExit();
if (process.ExitCode != 0)
{
throw new InvalidOperationException(
"BTSTask ExportBindings meldete Exitcode "
+ process.ExitCode
+ ": "
+ LastLines(output.ToString(), 8));
}
}
if (!File.Exists(destination) || new FileInfo(destination).Length == 0)
{
throw new InvalidDataException("BTSTask erzeugte keine Binding-Datei.");
}
}
private string ResolveBtsTaskPath()
{
var candidates = new List<string>();
if (!string.IsNullOrWhiteSpace(options.BtsTaskPath))
{
candidates.Add(options.BtsTaskPath);
}
if (!string.IsNullOrWhiteSpace(document.System.BizTalkInstallPath))
{
candidates.Add(Path.Combine(document.System.BizTalkInstallPath, "BTSTask.exe"));
}
var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
var programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86);
candidates.Add(Path.Combine(programFiles, "Microsoft BizTalk Server 2020", "BTSTask.exe"));
candidates.Add(Path.Combine(programFilesX86, "Microsoft BizTalk Server 2020", "BTSTask.exe"));
var path = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
candidates.AddRange(path.Split(Path.PathSeparator)
.Where(item => !string.IsNullOrWhiteSpace(item))
.Select(item => Path.Combine(item.Trim(), "BTSTask.exe")));
return candidates.FirstOrDefault(File.Exists);
}
private static XDocument LoadXml(string path)
{
var settings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Prohibit,
XmlResolver = null,
IgnoreComments = true
};
using (var stream = File.OpenRead(path))
using (var reader = XmlReader.Create(stream, settings))
{
return XDocument.Load(reader, LoadOptions.None);
}
}
private static bool LooksLikeSap(
string adapterName,
string address,
IEnumerable<NameValueRecord> properties)
{
if ((adapterName ?? string.Empty).IndexOf("SAP", StringComparison.OrdinalIgnoreCase) >= 0
|| (address ?? string.Empty).StartsWith("sap://", StringComparison.OrdinalIgnoreCase))
{
return true;
}
return properties.Any(item =>
item.Name.IndexOf("SAP", StringComparison.OrdinalIgnoreCase) >= 0
|| item.Value.IndexOf("sapBinding", StringComparison.OrdinalIgnoreCase) >= 0
|| item.Value.IndexOf("Microsoft.Adapters.SAP", StringComparison.OrdinalIgnoreCase) >= 0);
}
private static void AddProperties(
SapEndpointRecord endpoint,
IEnumerable<NameValueRecord> properties)
{
foreach (var property in properties)
{
AddUnique(endpoint.Properties, property.Name, property.Value, property.Source);
}
}
private static void AddUnique(
List<NameValueRecord> target,
string name,
string value,
string source)
{
if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(value))
{
return;
}
var safeValue = SensitiveDataSanitizer.RedactValue(name, value);
if (target.Any(item =>
string.Equals(item.Name, name, StringComparison.OrdinalIgnoreCase)
&& string.Equals(item.Value, safeValue, StringComparison.Ordinal)))
{
return;
}
target.Add(new NameValueRecord(name, safeValue, source));
}
private static string NormalizePropertyName(string name)
{
var normalized = (name ?? string.Empty).Trim();
var mappings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
{ "Client", "Client" },
{ "lang", "Language" },
{ "Language", "Language" },
{ "GwHost", "GatewayHost" },
{ "GwServ", "GatewayService" },
{ "ListenerDest", "ListenerDestination" },
{ "ListenerGwHost", "ListenerGatewayHost" },
{ "ListenerGwServ", "ListenerGatewayService" },
{ "ListenerProgramId", "ListenerProgramId" },
{ "UseSnc", "UseSnc" },
{ "SAPROUTER", "SapRouter" }
};
string mapped;
return mappings.TryGetValue(normalized, out mapped) ? mapped : normalized;
}
private static XElement Child(XElement parent, string localName)
{
return parent == null
? null
: parent.Elements().FirstOrDefault(item => IsName(item, localName));
}
private static string ChildValue(XElement parent, string localName)
{
var child = Child(parent, localName);
return child == null ? string.Empty : child.Value;
}
private static string Attribute(XElement element, string localName)
{
if (element == null)
{
return string.Empty;
}
var attribute = element.Attributes().FirstOrDefault(item =>
string.Equals(item.Name.LocalName, localName, StringComparison.OrdinalIgnoreCase));
return attribute == null ? string.Empty : attribute.Value;
}
private static bool IsName(XElement element, string localName)
{
return string.Equals(
element.Name.LocalName,
localName,
StringComparison.OrdinalIgnoreCase);
}
private static bool HasAncestor(XElement element, string localName)
{
return element.Ancestors().Any(item => IsName(item, localName));
}
private static string FirstNonEmpty(params string[] values)
{
return values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value)) ?? string.Empty;
}
private static bool IsTrue(string value)
{
return string.Equals(value, "true", StringComparison.OrdinalIgnoreCase)
|| string.Equals(value, "1", StringComparison.OrdinalIgnoreCase)
|| string.Equals(value, "yes", StringComparison.OrdinalIgnoreCase)
|| string.Equals(value, "ja", StringComparison.OrdinalIgnoreCase);
}
private static string Decode(string value)
{
try
{
return Uri.UnescapeDataString((value ?? string.Empty).Replace("+", " "));
}
catch (UriFormatException)
{
return value ?? string.Empty;
}
}
private static string EscapeCommandArgument(string value)
{
return (value ?? string.Empty).Replace("\"", string.Empty)
.Replace("\r", string.Empty)
.Replace("\n", string.Empty);
}
private static string LastLines(string text, int count)
{
var lines = (text ?? string.Empty)
.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries);
return string.Join(" | ", lines.Skip(Math.Max(0, lines.Length - count)));
}
private static void TryDelete(string path)
{
try
{
if (File.Exists(path))
{
File.Delete(path);
}
}
catch (IOException)
{
}
catch (UnauthorizedAccessException)
{
}
}
}
}
@@ -0,0 +1,433 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Management;
using BizTalkSapEnvironmentInventory.Infrastructure;
using BizTalkSapEnvironmentInventory.Models;
namespace BizTalkSapEnvironmentInventory.Collectors
{
/// <summary>
/// Liest Anwendungen, Artefakte, Adapter und Hosts aus dem BizTalk-WMI-Provider.
/// </summary>
internal sealed class BizTalkWmiCollector
{
private const string NamespacePath = @"root\MicrosoftBizTalkServer";
private readonly InventoryDocument document;
private readonly ConsoleFileLogger logger;
private readonly ManagementScope scope;
private readonly TimeSpan timeout;
private readonly int maxArtifactDetailsPerType;
public BizTalkWmiCollector(
InventoryDocument document,
ConsoleFileLogger logger,
int timeoutSeconds,
int maxArtifactDetailsPerType)
{
this.document = document;
this.logger = logger;
timeout = TimeSpan.FromSeconds(Math.Max(5, timeoutSeconds));
this.maxArtifactDetailsPerType = Math.Max(100, maxArtifactDetailsPerType);
scope = new ManagementScope(@"\\" + Environment.MachineName + @"\" + NamespacePath);
scope.Options.Timeout = timeout;
}
/// <summary>
/// Baut die vollständige Anwendungsliste auf und ergänzt verfügbare Artefaktbeziehungen.
/// </summary>
public void Collect()
{
scope.Connect();
CollectApplications();
var artifactClasses = new[]
{
new ClassDescriptor("MSBTS_Orchestration", "Orchestration"),
new ClassDescriptor("MSBTS_SendPort", "SendPort"),
new ClassDescriptor("MSBTS_SendPortGroup", "SendPortGroup"),
new ClassDescriptor("MSBTS_ReceivePort", "ReceivePort"),
new ClassDescriptor("MSBTS_ReceiveLocation", "ReceiveLocation"),
new ClassDescriptor("MSBTS_Assembly", "Assembly"),
new ClassDescriptor("MSBTS_Schema", "Schema"),
new ClassDescriptor("MSBTS_Map", "Map"),
new ClassDescriptor("MSBTS_Pipeline", "Pipeline")
};
foreach (var descriptor in artifactClasses)
{
TryCollectArtifacts(descriptor);
}
TryCollectComponents("MSBTS_AdapterSetting", "Adapter", document.Adapters, true);
TryCollectComponents("MSBTS_ReceiveHandler", "ReceiveHandler", document.Hosts, false);
if (!TryCollectComponents("MSBTS_SendHandler2", "SendHandler", document.Hosts, false))
{
TryCollectComponents("MSBTS_SendHandler", "SendHandler", document.Hosts, false);
}
TryCollectComponents("MSBTS_HostSetting", "Host", document.Hosts, false);
TryCollectComponents("MSBTS_HostInstance", "HostInstance", document.Hosts, false);
TryCollectWmiSapEndpoints("MSBTS_SendPort", "Senden");
TryCollectWmiSapEndpoints("MSBTS_ReceiveLocation", "Empfangen");
document.Applications.Sort((left, right) =>
string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase));
}
private void CollectApplications()
{
var rows = Query("SELECT * FROM MSBTS_Application");
foreach (var row in rows)
{
var name = First(row, "Name", "ApplicationName");
if (string.IsNullOrWhiteSpace(name))
{
continue;
}
GetOrCreateApplication(name).Description = First(row, "Description");
GetOrCreateApplication(name).Status = FormatStatus(First(row, "Status"));
}
if (document.Applications.Count == 0)
{
throw new InvalidOperationException(
"WMI lieferte keine BizTalk-Anwendungen. Berechtigungen und BizTalk-Gruppe prüfen.");
}
logger.Info(document.Applications.Count + " BizTalk-Anwendung(en) über WMI gefunden.");
}
private void TryCollectArtifacts(ClassDescriptor descriptor)
{
try
{
var count = 0;
foreach (var row in Query("SELECT * FROM " + descriptor.ClassName))
{
if (count >= maxArtifactDetailsPerType)
{
document.Findings.Add(new Finding
{
Severity = "Warnung",
Area = descriptor.DisplayName,
Message = "Artefaktdetails wurden bei "
+ maxArtifactDetailsPerType
+ " Einträgen begrenzt.",
RecommendedAction = "MaxArtifactDetailsPerType bei Bedarf kontrolliert erhöhen.",
Owner = "BizTalk-Betrieb"
});
break;
}
var applicationName = First(row, "ApplicationName", "Application");
var name = First(
row,
"Name",
"AssemblyName",
"FullName",
"ReceivePortName",
"OrchestrationName");
if (string.IsNullOrWhiteSpace(name))
{
continue;
}
var artifact = new ArtifactRecord
{
Type = descriptor.DisplayName,
Name = name,
ApplicationName = applicationName,
Status = FormatStatus(First(row, "Status", "ServiceStatus"))
};
AddSelectedProperties(row, artifact.Properties);
if (!string.IsNullOrWhiteSpace(applicationName))
{
GetOrCreateApplication(applicationName).Artifacts.Add(artifact);
}
else
{
var defaultApplication = document.Applications.FirstOrDefault(item =>
string.Equals(item.Name, "BizTalk Application 1", StringComparison.OrdinalIgnoreCase));
if (defaultApplication != null)
{
defaultApplication.Artifacts.Add(artifact);
}
}
count++;
}
logger.Info(descriptor.DisplayName + ": " + count + " Artefakt(e).");
}
catch (ManagementException exception)
{
AddOptionalWmiFinding(descriptor.ClassName, exception);
}
}
private bool TryCollectComponents(
string className,
string category,
List<ComponentRecord> target,
bool onlySap)
{
try
{
foreach (var row in Query("SELECT * FROM " + className))
{
if (onlySap && !LooksLikeSap(row))
{
continue;
}
var component = new ComponentRecord
{
Category = category,
Name = First(row, "Name", "AdapterName", "HostName", "RunningServer"),
Status = FormatStatus(First(row, "Status", "ServiceState", "HostType"))
};
AddSelectedProperties(row, component.Properties);
if (!string.IsNullOrWhiteSpace(component.Name))
{
target.Add(component);
}
}
return true;
}
catch (ManagementException exception)
{
AddOptionalWmiFinding(className, exception);
return false;
}
}
private void TryCollectWmiSapEndpoints(string className, string direction)
{
try
{
foreach (var row in Query("SELECT * FROM " + className))
{
if (!LooksLikeSap(row))
{
continue;
}
var endpointName = First(row, "Name");
if (document.SapEndpoints.Any(item =>
string.Equals(item.Direction, direction, StringComparison.OrdinalIgnoreCase)
&& string.Equals(item.Name, endpointName, StringComparison.OrdinalIgnoreCase)))
{
continue;
}
var endpoint = new SapEndpointRecord
{
ApplicationName = First(row, "ApplicationName"),
Direction = direction,
Name = endpointName,
ParentPortName = First(row, "ReceivePortName"),
AdapterName = First(
row,
"PTTransportType",
"AdapterName",
"TransportType"),
HostName = First(row, "HostName", "SendHandler", "ReceiveHandler"),
Status = FormatStatus(First(row, "Status", "IsDisabled")),
Address = SensitiveDataSanitizer.SanitizeText(First(
row,
"PTAddress",
"InboundTransportURL",
"Address")),
Source = "WMI-Fallback"
};
AddSelectedProperties(row, endpoint.Properties);
document.SapEndpoints.Add(endpoint);
}
}
catch (ManagementException exception)
{
AddOptionalWmiFinding(className + " SAP-Fallback", exception);
}
}
private List<ManagementObject> Query(string query)
{
var options = new EnumerationOptions
{
ReturnImmediately = false,
Rewindable = false,
Timeout = timeout
};
using (var searcher = new ManagementObjectSearcher(
scope,
new ObjectQuery(query),
options))
{
return searcher.Get().Cast<ManagementObject>().ToList();
}
}
private ApplicationRecord GetOrCreateApplication(string name)
{
var application = document.Applications.FirstOrDefault(item =>
string.Equals(item.Name, name, StringComparison.OrdinalIgnoreCase));
if (application != null)
{
return application;
}
application = new ApplicationRecord
{
Name = name,
Status = "Unbekannt"
};
document.Applications.Add(application);
return application;
}
private static void AddSelectedProperties(
ManagementBaseObject row,
List<NameValueRecord> target)
{
var preferred = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"ApplicationName", "Name", "Description", "Status", "HostName",
"ReceivePortName", "AdapterName", "PTTransportType", "STTransportType",
"PTAddress", "STAddress", "InboundTransportURL", "IsDisabled",
"IsTwoWay", "SendPipeline", "ReceivePipeline", "AssemblyName",
"FullName", "TargetNameSpace", "RootName", "MgmtDbNameOverride",
"MgmtDbServerOverride", "IsDefault", "HostType", "RunningServer",
"NTGroupName", "IsHost32BitOnly", "AuthTrusted"
};
foreach (PropertyData property in row.Properties)
{
if (!preferred.Contains(property.Name) || property.Value == null)
{
continue;
}
var value = ConvertValue(property.Value);
target.Add(new NameValueRecord(
property.Name,
SensitiveDataSanitizer.RedactValue(property.Name, value),
"WMI"));
}
}
private static bool LooksLikeSap(ManagementBaseObject row)
{
foreach (PropertyData property in row.Properties)
{
if (property.Value == null)
{
continue;
}
var value = ConvertValue(property.Value);
if (value.IndexOf("WCF-SAP", StringComparison.OrdinalIgnoreCase) >= 0
|| value.IndexOf("Microsoft.Adapters.SAP", StringComparison.OrdinalIgnoreCase) >= 0
|| value.StartsWith("sap://", StringComparison.OrdinalIgnoreCase)
|| string.Equals(value, "SAP", StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
private static string First(ManagementBaseObject row, params string[] names)
{
foreach (var name in names)
{
try
{
var value = row[name];
if (value != null)
{
var text = ConvertValue(value);
if (!string.IsNullOrWhiteSpace(text))
{
return text;
}
}
}
catch (ManagementException)
{
// Property is not part of this BizTalk WMI class/version.
}
}
return string.Empty;
}
private static string ConvertValue(object value)
{
var array = value as Array;
if (array != null && !(value is byte[]))
{
return string.Join(
", ",
array.Cast<object>().Select(item =>
Convert.ToString(item, CultureInfo.InvariantCulture)));
}
return Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty;
}
private static string FormatStatus(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return "Unbekannt";
}
switch (value.Trim())
{
case "1":
return "Gestoppt (1)";
case "2":
return "Gestartet (2)";
case "3":
return "Teilweise gestartet (3)";
case "True":
return "Ja";
case "False":
return "Nein";
default:
return value;
}
}
private void AddOptionalWmiFinding(string className, ManagementException exception)
{
document.Findings.Add(new Finding
{
Severity = "Hinweis",
Area = "WMI " + className,
Message = "Optionale WMI-Klasse konnte nicht gelesen werden: " + exception.Message,
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);
}
private sealed class ClassDescriptor
{
public ClassDescriptor(string className, string displayName)
{
ClassName = className;
DisplayName = displayName;
}
public string ClassName { get; private set; }
public string DisplayName { get; private set; }
}
}
}
@@ -0,0 +1,535 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Win32;
using BizTalkSapEnvironmentInventory.Infrastructure;
using BizTalkSapEnvironmentInventory.Models;
namespace BizTalkSapEnvironmentInventory.Collectors
{
/// <summary>
/// Dokumentiert SAP-NCo-/SNC-Installationen, Architektur und sichere Security-Metadaten.
/// </summary>
internal sealed class SapRuntimeCollector
{
private static readonly string[] RuntimeFileNames =
{
"sapnco.dll",
"sapnco_utils.dll",
"sapcrypto.dll",
"sapgenpse.exe",
"libsapcrypto.dll",
"sapgsskrb5.dll"
};
private readonly InventoryDocument document;
private readonly ConsoleFileLogger logger;
private readonly bool includeCertificates;
public SapRuntimeCollector(
InventoryDocument document,
ConsoleFileLogger logger,
bool includeCertificates)
{
this.document = document;
this.logger = logger;
this.includeCertificates = includeCertificates;
}
/// <summary>
/// Führt begrenzte Registry-, Pfad-, Datei- und Zertifikatserfassung aus.
/// </summary>
public void Collect()
{
CollectInstalledProducts();
var configuredPaths = CollectConfiguredSecurityPaths();
CollectEnvironment(document.RuntimeComponents);
CollectRuntimeFiles(configuredPaths);
CollectSecurityFiles(configuredPaths);
if (includeCertificates)
{
CollectCertificates();
}
if (!document.RuntimeComponents.Any(item =>
string.Equals(
item.Category,
"SAP Runtime-Datei",
StringComparison.OrdinalIgnoreCase)
|| item.Category.StartsWith(
"Installierte SAP",
StringComparison.OrdinalIgnoreCase)))
{
document.Findings.Add(new Finding
{
Severity = "Warnung",
Area = "SAP Runtime",
Message = "Keine SAP-NCo-/SNC-Laufzeitdatei oder installierte SAP-Komponente wurde automatisch gefunden.",
RecommendedAction = "Installationspfad, GAC und Architektur der WCF-SAP-/NCo-Komponenten manuell bestätigen.",
Owner = "BizTalk-Betrieb"
});
}
}
private void CollectInstalledProducts()
{
foreach (var view in new[] { RegistryView.Registry64, RegistryView.Registry32 })
{
using (var baseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, view))
{
CollectUninstallKey(
baseKey,
@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall",
view);
}
}
}
private void CollectUninstallKey(RegistryKey baseKey, string path, RegistryView view)
{
using (var uninstall = baseKey.OpenSubKey(path, false))
{
if (uninstall == null)
{
return;
}
foreach (var subKeyName in uninstall.GetSubKeyNames())
{
using (var product = uninstall.OpenSubKey(subKeyName, false))
{
if (product == null)
{
continue;
}
var name = Convert.ToString(
product.GetValue("DisplayName"),
CultureInfo.InvariantCulture);
if (!IsRelevantProduct(name))
{
continue;
}
var component = new ComponentRecord
{
Category = "Installierte SAP/BizTalk-Adapter-Komponente",
Name = name,
Status = "Installiert"
};
AddIfPresent(component, "Version", product.GetValue("DisplayVersion"), "Registry " + view);
AddIfPresent(component, "Hersteller", product.GetValue("Publisher"), "Registry " + view);
AddIfPresent(component, "Installationspfad", product.GetValue("InstallLocation"), "Registry " + view);
AddIfPresent(component, "Architektur", view.ToString(), "Registry");
document.RuntimeComponents.Add(component);
}
}
}
}
private HashSet<string> CollectConfiguredSecurityPaths()
{
var result = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var endpoint in document.SapEndpoints)
{
foreach (var property in endpoint.Properties)
{
if (property.Name.IndexOf("SncLibrary", StringComparison.OrdinalIgnoreCase) >= 0
|| property.Name.IndexOf("LibraryPath", StringComparison.OrdinalIgnoreCase) >= 0
|| property.Name.IndexOf("SecuDir", StringComparison.OrdinalIgnoreCase) >= 0
|| property.Name.IndexOf("Pse", StringComparison.OrdinalIgnoreCase) >= 0)
{
AddPath(result, property.Value);
}
}
}
AddPath(result, Environment.GetEnvironmentVariable("SNC_LIB"));
AddPath(result, Environment.GetEnvironmentVariable("SECUDIR"));
AddPath(result, Environment.GetEnvironmentVariable("SAPNWRFC_HOME"));
return result;
}
private static void CollectEnvironment(List<ComponentRecord> target)
{
var component = new ComponentRecord
{
Category = "SAP/SNC-Umgebungsvariablen",
Name = "Prozessumgebung",
Status = "Erfasst"
};
foreach (var variable in new[] { "SNC_LIB", "SECUDIR", "SAPNWRFC_HOME", "SAP_CODEPAGE" })
{
var value = Environment.GetEnvironmentVariable(variable);
component.Properties.Add(new NameValueRecord(
variable,
string.IsNullOrWhiteSpace(value) ? "Nicht gesetzt" : value,
"Prozessumgebung"));
}
target.Add(component);
}
private void CollectRuntimeFiles(HashSet<string> configuredPaths)
{
var candidates = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var path in configuredPaths)
{
if (File.Exists(path) && IsRuntimeFile(path))
{
candidates.Add(path);
}
}
var roots = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
AppDomain.CurrentDomain.BaseDirectory,
document.System.BizTalkInstallPath,
Environment.GetEnvironmentVariable("SAPNWRFC_HOME"),
Environment.GetEnvironmentVariable("SECUDIR"),
Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
"SAP"),
Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86),
"SAP"),
Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
"Microsoft BizTalk Adapter Pack"),
Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86),
"Microsoft BizTalk Adapter Pack")
};
foreach (var root in roots.Where(Directory.Exists))
{
FindRuntimeFiles(root, candidates);
}
foreach (var path in candidates.OrderBy(item => item, StringComparer.OrdinalIgnoreCase))
{
var component = new ComponentRecord
{
Category = "SAP Runtime-Datei",
Name = Path.GetFileName(path),
Status = "Vorhanden"
};
component.Properties.Add(new NameValueRecord("Pfad", path));
try
{
var version = FileVersionInfo.GetVersionInfo(path);
component.Properties.Add(new NameValueRecord(
"Dateiversion",
EmptyAsUnknown(version.FileVersion)));
component.Properties.Add(new NameValueRecord(
"Produktversion",
EmptyAsUnknown(version.ProductVersion)));
component.Properties.Add(new NameValueRecord(
"Architektur",
ReadPeArchitecture(path)));
var file = new FileInfo(path);
component.Properties.Add(new NameValueRecord(
"Geändert (UTC)",
file.LastWriteTimeUtc.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)));
}
catch (Exception exception) when (
exception is IOException
|| exception is UnauthorizedAccessException
|| exception is ArgumentException)
{
component.Status = "Metadaten teilweise";
component.Properties.Add(new NameValueRecord("Fehler", exception.Message));
}
document.RuntimeComponents.Add(component);
}
}
private void CollectSecurityFiles(HashSet<string> configuredPaths)
{
foreach (var path in configuredPaths.OrderBy(item => item, StringComparer.OrdinalIgnoreCase))
{
if (Directory.Exists(path))
{
foreach (var file in SafeEnumerateFiles(path)
.Where(IsSecurityFile)
.Take(100))
{
AddSecurityFile(file, "SNC/PSE-Datei");
}
}
else if (File.Exists(path) && IsSecurityFile(path))
{
AddSecurityFile(path, "SNC/PSE-Datei");
}
else if (LooksLikePath(path))
{
document.SecurityMaterials.Add(new SecurityMaterialRecord
{
Kind = "Konfigurierter SNC-/Security-Pfad",
Name = Path.GetFileName(path),
Location = path,
Exists = "Nein oder nicht lesbar",
Details = "Pfad stammt aus Binding oder Umgebungsvariable."
});
}
}
}
private void AddSecurityFile(string path, string kind)
{
var record = new SecurityMaterialRecord
{
Kind = kind,
Name = Path.GetFileName(path),
Location = path,
Exists = "Ja"
};
try
{
var file = new FileInfo(path);
record.Details = string.Format(
CultureInfo.InvariantCulture,
"Größe {0} Bytes; geändert (UTC) {1:yyyy-MM-dd HH:mm:ss}",
file.Length,
file.LastWriteTimeUtc);
record.Fingerprint = ComputeSha256(path);
}
catch (Exception exception) when (
exception is IOException
|| exception is UnauthorizedAccessException
|| exception is CryptographicException)
{
record.Details = "Metadaten nur teilweise lesbar: " + exception.Message;
}
document.SecurityMaterials.Add(record);
}
private void CollectCertificates()
{
using (var store = new X509Store(StoreName.My, StoreLocation.LocalMachine))
{
store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);
foreach (var certificate in store.Certificates.Cast<X509Certificate2>()
.Where(IsSapRelatedCertificate))
{
document.SecurityMaterials.Add(new SecurityMaterialRecord
{
Kind = "LocalMachine\\My Zertifikat (SAP/SNC-Indikator)",
Name = certificate.Subject,
Location = "LocalMachine\\My",
Exists = "Ja",
Fingerprint = certificate.Thumbprint,
ValidFrom = certificate.NotBefore.ToString(
"yyyy-MM-dd HH:mm:ss",
CultureInfo.InvariantCulture),
ValidTo = certificate.NotAfter.ToString(
"yyyy-MM-dd HH:mm:ss",
CultureInfo.InvariantCulture),
Details = "Issuer: " + certificate.Issuer
+ "; privater Schlüssel vorhanden: "
+ (certificate.HasPrivateKey ? "Ja" : "Nein")
});
}
}
}
private static void FindRuntimeFiles(string root, HashSet<string> target)
{
var pending = new Queue<PathDepth>();
pending.Enqueue(new PathDepth(root, 0));
var visited = 0;
while (pending.Count > 0 && visited < 2000)
{
var current = pending.Dequeue();
visited++;
foreach (var file in SafeEnumerateFiles(current.Path))
{
if (IsRuntimeFile(file))
{
target.Add(file);
}
}
if (current.Depth >= 5)
{
continue;
}
foreach (var directory in SafeEnumerateDirectories(current.Path))
{
pending.Enqueue(new PathDepth(directory, current.Depth + 1));
}
}
}
private static IEnumerable<string> SafeEnumerateFiles(string path)
{
try
{
return Directory.EnumerateFiles(path).ToArray();
}
catch (IOException)
{
return new string[0];
}
catch (UnauthorizedAccessException)
{
return new string[0];
}
}
private static IEnumerable<string> SafeEnumerateDirectories(string path)
{
try
{
return Directory.EnumerateDirectories(path).ToArray();
}
catch (IOException)
{
return new string[0];
}
catch (UnauthorizedAccessException)
{
return new string[0];
}
}
private static void AddPath(HashSet<string> target, string raw)
{
if (string.IsNullOrWhiteSpace(raw)
|| raw.IndexOf("***REDACTED***", StringComparison.OrdinalIgnoreCase) >= 0
|| raw.StartsWith("Konfiguriert", StringComparison.OrdinalIgnoreCase))
{
return;
}
var expanded = Environment.ExpandEnvironmentVariables(raw.Trim().Trim('"'));
if (LooksLikePath(expanded))
{
target.Add(expanded);
}
}
private static bool LooksLikePath(string value)
{
return !string.IsNullOrWhiteSpace(value)
&& (Path.IsPathRooted(value)
|| value.IndexOf('\\') >= 0
|| value.IndexOf('/') >= 0);
}
private static bool IsRelevantProduct(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
return false;
}
return name.IndexOf("SAP .NET Connector", StringComparison.OrdinalIgnoreCase) >= 0
|| name.IndexOf("BizTalk Adapter Pack", StringComparison.OrdinalIgnoreCase) >= 0
|| name.IndexOf("WCF LOB Adapter", StringComparison.OrdinalIgnoreCase) >= 0
|| name.IndexOf("SAP Secure", StringComparison.OrdinalIgnoreCase) >= 0
|| name.IndexOf("SAP Cryptographic", StringComparison.OrdinalIgnoreCase) >= 0;
}
private static bool IsRuntimeFile(string path)
{
var name = Path.GetFileName(path);
return RuntimeFileNames.Any(item =>
string.Equals(item, name, StringComparison.OrdinalIgnoreCase));
}
private static bool IsSecurityFile(string path)
{
var extension = Path.GetExtension(path);
return string.Equals(extension, ".pse", StringComparison.OrdinalIgnoreCase)
|| string.Equals(extension, ".crt", StringComparison.OrdinalIgnoreCase)
|| string.Equals(extension, ".cer", StringComparison.OrdinalIgnoreCase)
|| string.Equals(extension, ".pem", StringComparison.OrdinalIgnoreCase);
}
private static bool IsSapRelatedCertificate(X509Certificate2 certificate)
{
var text = string.Join(
" ",
certificate.Subject,
certificate.Issuer,
certificate.FriendlyName);
return text.IndexOf("SAP", StringComparison.OrdinalIgnoreCase) >= 0
|| text.IndexOf("SNC", StringComparison.OrdinalIgnoreCase) >= 0;
}
private static string ComputeSha256(string path)
{
using (var algorithm = SHA256.Create())
using (var stream = File.OpenRead(path))
{
return string.Concat(algorithm.ComputeHash(stream)
.Select(value => value.ToString("X2", CultureInfo.InvariantCulture)));
}
}
private static string ReadPeArchitecture(string path)
{
using (var stream = File.OpenRead(path))
using (var reader = new BinaryReader(stream))
{
if (reader.ReadUInt16() != 0x5A4D)
{
return "Kein PE-Format";
}
stream.Position = 0x3C;
var peOffset = reader.ReadInt32();
stream.Position = peOffset;
if (reader.ReadUInt32() != 0x00004550)
{
return "Unbekanntes PE-Format";
}
var machine = reader.ReadUInt16();
switch (machine)
{
case 0x014c:
return "x86/AnyCPU PE";
case 0x8664:
return "x64";
case 0x0200:
return "IA64";
case 0xAA64:
return "ARM64";
default:
return "PE Machine 0x" + machine.ToString("X4", CultureInfo.InvariantCulture);
}
}
}
private static void AddIfPresent(
ComponentRecord component,
string name,
object value,
string source)
{
var text = Convert.ToString(value, CultureInfo.InvariantCulture);
if (!string.IsNullOrWhiteSpace(text))
{
component.Properties.Add(new NameValueRecord(name, text, source));
}
}
private static string EmptyAsUnknown(string value)
{
return string.IsNullOrWhiteSpace(value) ? "Unbekannt" : value;
}
private sealed class PathDepth
{
public PathDepth(string path, int depth)
{
Path = path;
Depth = depth;
}
public string Path { get; private set; }
public int Depth { get; private set; }
}
}
}
@@ -0,0 +1,313 @@
using System;
using System.Globalization;
using System.Management;
using System.Reflection;
using Microsoft.Win32;
using BizTalkSapEnvironmentInventory.Configuration;
using BizTalkSapEnvironmentInventory.Models;
namespace BizTalkSapEnvironmentInventory.Collectors
{
/// <summary>
/// Erfasst lokale Windows-, BizTalk-Installations- und Management-DB-Metadaten.
/// </summary>
internal sealed class SystemCollector
{
private const string BizTalkKey = @"SOFTWARE\Microsoft\BizTalk Server\3.0";
private const string AdministrationKey = BizTalkKey + @"\Administration";
private readonly CommandLineOptions options;
public SystemCollector(CommandLineOptions options)
{
this.options = options;
}
/// <summary>
/// Liest ausschließlich lokale WMI- und Registry-Daten.
/// </summary>
public void Collect(InventoryDocument document)
{
document.ComputerName = Environment.MachineName;
document.ToolVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString();
document.System.Properties.Add(new NameValueRecord(
"Ausführender Benutzer",
Environment.UserDomainName + "\\" + Environment.UserName));
document.System.Properties.Add(new NameValueRecord(
"64-Bit-Betriebssystem",
Environment.Is64BitOperatingSystem ? "Ja" : "Nein"));
document.System.Properties.Add(new NameValueRecord(
"64-Bit-Prozess",
Environment.Is64BitProcess ? "Ja" : "Nein"));
document.System.Properties.Add(new NameValueRecord(
".NET Runtime",
Environment.Version.ToString()));
CollectOperatingSystem(document);
CollectBizTalkRegistry(document);
CollectBizTalkGroupSetting(document);
if (!string.IsNullOrWhiteSpace(options.ManagementServer))
{
document.System.ManagementServer = options.ManagementServer;
}
if (!string.IsNullOrWhiteSpace(options.ManagementDatabase))
{
document.System.ManagementDatabase = options.ManagementDatabase;
}
else if (string.IsNullOrWhiteSpace(document.System.ManagementDatabase))
{
document.System.ManagementDatabase = "BizTalkMgmtDb";
}
document.System.Properties.Add(new NameValueRecord(
"BizTalk Management SQL Server",
EmptyAsUnknown(document.System.ManagementServer)));
document.System.Properties.Add(new NameValueRecord(
"BizTalk Management Database",
EmptyAsUnknown(document.System.ManagementDatabase)));
if (!string.Equals(document.EnvironmentName, "ACC", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(document.EnvironmentName, "PROD", StringComparison.OrdinalIgnoreCase))
{
document.Findings.Add(new Finding
{
Severity = "Warnung",
Area = "Aufruf",
Message = "Die Umgebung ist weder ACC noch PROD: " + document.EnvironmentName,
RecommendedAction = "Umgebungsparameter und Zielserver vor der Ablage des Berichts bestätigen.",
Owner = "BizTalk-Betrieb"
});
}
}
private static void CollectOperatingSystem(InventoryDocument document)
{
using (var searcher = new ManagementObjectSearcher(
"root\\cimv2",
"SELECT Caption,Version,BuildNumber,OSArchitecture,LastBootUpTime FROM Win32_OperatingSystem"))
{
foreach (ManagementObject item in searcher.Get())
{
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;
}
}
}
private static 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))
{
if (product != null)
{
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");
}
}
}
if (string.IsNullOrWhiteSpace(document.System.BizTalkInstallPath))
{
document.Findings.Add(new Finding
{
Severity = "Warnung",
Area = "BizTalk-Installation",
Message = "BizTalk-Installationspfad wurde nicht in der Registry gefunden.",
RecommendedAction = "Prüfen, ob das Tool lokal auf einem BizTalk Server 2020 ausgeführt wird.",
Owner = "BizTalk-Betrieb"
});
}
}
private static void CollectBizTalkGroupSetting(InventoryDocument document)
{
try
{
using (var searcher = new ManagementObjectSearcher(
@"root\MicrosoftBizTalkServer",
"SELECT Name,MgmtDbServerName,MgmtDbName,BizTalkAdministratorGroup,"
+ "BizTalkOperatorGroup,BizTalkReadOnlyUserGroup,SSOServerName "
+ "FROM MSBTS_GroupSetting"))
{
foreach (ManagementObject item in searcher.Get())
{
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,
"BizTalkAdministratorGroup",
"BizTalk Administratorengruppe");
AddWmiValue(
document,
item,
"BizTalkOperatorGroup",
"BizTalk Operatorengruppe");
AddWmiValue(
document,
item,
"BizTalkReadOnlyUserGroup",
"BizTalk ReadOnly-Gruppe");
AddWmiValue(document, item, "SSOServerName", "Enterprise SSO Server");
break;
}
}
}
catch (ManagementException 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"
});
}
}
private static void AddWmiValue(
InventoryDocument document,
ManagementBaseObject item,
string property,
string displayName)
{
var value = Value(item, property);
if (!string.IsNullOrWhiteSpace(value))
{
document.System.Properties.Add(
new NameValueRecord(displayName, value, "MSBTS_GroupSetting"));
}
}
private static void AddRegistryValue(
InventoryDocument document,
RegistryKey key,
string valueName,
string displayName,
RegistryView view)
{
var value = Convert.ToString(key.GetValue(valueName), CultureInfo.InvariantCulture);
if (!string.IsNullOrWhiteSpace(value)
&& !document.System.Properties.Exists(item =>
string.Equals(item.Name, displayName, StringComparison.OrdinalIgnoreCase)))
{
document.System.Properties.Add(new NameValueRecord(
displayName,
value,
"Registry " + view));
}
}
private static string FirstRegistryValue(RegistryKey key, params string[] names)
{
foreach (var name in names)
{
var value = Convert.ToString(key.GetValue(name), CultureInfo.InvariantCulture);
if (!string.IsNullOrWhiteSpace(value))
{
return value.Trim();
}
}
return string.Empty;
}
private static string Value(ManagementBaseObject item, string property)
{
try
{
return Convert.ToString(item[property], CultureInfo.InvariantCulture) ?? string.Empty;
}
catch (ManagementException)
{
return string.Empty;
}
}
private static string ConvertWmiDate(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return "Unbekannt";
}
try
{
return ManagementDateTimeConverter.ToDateTime(value)
.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture);
}
catch (ArgumentOutOfRangeException)
{
return value;
}
}
private static string EmptyAsUnknown(string value)
{
return string.IsNullOrWhiteSpace(value) ? "Nicht ermittelt" : value;
}
}
}
@@ -0,0 +1,152 @@
using System;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
namespace BizTalkSapEnvironmentInventory.Configuration
{
/// <summary>
/// Validierte, unveränderliche Sicht auf die Kommandozeilenparameter.
/// </summary>
internal sealed class CommandLineOptions
{
private static readonly Regex SafeEnvironment =
new Regex("[^A-Z0-9_-]+", RegexOptions.Compiled | RegexOptions.CultureInvariant);
public string EnvironmentName { get; private set; }
public string OutputDirectory { get; private set; }
public string ManagementServer { get; private set; }
public string ManagementDatabase { get; private set; }
public string BtsTaskPath { get; private set; }
public string BindingFilePath { get; private set; }
public bool SelfTest { get; private set; }
public bool ShowHelp { get; private set; }
/// <summary>
/// Parst die Kommandozeile, normalisiert die Umgebung und löst lokale Pfade auf.
/// </summary>
public static CommandLineOptions Parse(string[] args)
{
var result = new CommandLineOptions();
for (var index = 0; index < args.Length; index++)
{
var argument = args[index];
switch (argument.ToLowerInvariant())
{
case "--environment":
result.EnvironmentName = RequireValue(args, ref index, argument);
break;
case "--output":
result.OutputDirectory = RequireValue(args, ref index, argument);
break;
case "--management-server":
result.ManagementServer = RequireValue(args, ref index, argument);
break;
case "--management-database":
result.ManagementDatabase = RequireValue(args, ref index, argument);
break;
case "--btstask":
result.BtsTaskPath = RequireValue(args, ref index, argument);
break;
case "--binding-file":
result.BindingFilePath = RequireValue(args, ref index, argument);
break;
case "--self-test":
result.SelfTest = true;
break;
case "--help":
case "-h":
case "/?":
result.ShowHelp = true;
break;
default:
throw new ArgumentException("Unbekannte Option: " + argument);
}
}
if (result.SelfTest || result.ShowHelp)
{
return result;
}
if (string.IsNullOrWhiteSpace(result.EnvironmentName))
{
throw new ArgumentException("--environment fehlt.");
}
result.EnvironmentName = SafeEnvironment.Replace(
result.EnvironmentName.Trim().ToUpperInvariant(),
"-").Trim('-');
if (string.IsNullOrWhiteSpace(result.EnvironmentName))
{
throw new ArgumentException("--environment enthält keinen gültigen Namen.");
}
if (string.IsNullOrWhiteSpace(result.OutputDirectory))
{
result.OutputDirectory = Path.Combine(
Environment.CurrentDirectory,
"BizTalk-SAP-Dokumentation",
result.EnvironmentName);
}
result.OutputDirectory = Path.GetFullPath(
Environment.ExpandEnvironmentVariables(result.OutputDirectory));
if (!string.IsNullOrWhiteSpace(result.BindingFilePath))
{
result.BindingFilePath = Path.GetFullPath(
Environment.ExpandEnvironmentVariables(result.BindingFilePath));
if (!File.Exists(result.BindingFilePath))
{
throw new FileNotFoundException(
"Angegebene Binding-Datei wurde nicht gefunden.",
result.BindingFilePath);
}
}
if (!string.IsNullOrWhiteSpace(result.BtsTaskPath))
{
result.BtsTaskPath = Path.GetFullPath(
Environment.ExpandEnvironmentVariables(result.BtsTaskPath));
}
return result;
}
/// <summary>
/// Liefert die konsolenfreundliche Aufrufhilfe.
/// </summary>
public static string Usage()
{
return string.Join(Environment.NewLine, new[]
{
"BEW BizTalk SAP Environment Inventory",
string.Empty,
"Aufruf:",
" BizTalkSapEnvironmentInventory.exe --environment ACC [--output PFAD]",
" BizTalkSapEnvironmentInventory.exe --environment PROD [--output PFAD]",
string.Empty,
"Optionen:",
" --management-server NAME SQL-Server der BizTalkMgmtDb; normalerweise automatisch.",
" --management-database NAME Management-Datenbank; Default BizTalkMgmtDb.",
" --btstask DATEI Expliziter Pfad zu BTSTask.exe.",
" --binding-file DATEI Vorhandenen Binding-Export offline auswerten.",
" --self-test Sicherheits-, Parser- und DOCX-Selbsttest.",
" --help Diese Hilfe."
});
}
private static string RequireValue(string[] args, ref int index, string option)
{
index++;
if (index >= args.Length || args[index].StartsWith("--", StringComparison.Ordinal))
{
throw new ArgumentException("Wert für " + option + " fehlt.");
}
return args[index];
}
}
}
@@ -0,0 +1,60 @@
using System;
using System.Globalization;
using System.IO;
using System.Text;
namespace BizTalkSapEnvironmentInventory.Infrastructure
{
/// <summary>
/// Schreibt identische, zeitgestempelte Fortschrittsmeldungen auf Konsole und in die Logdatei.
/// </summary>
internal sealed class ConsoleFileLogger : IDisposable
{
private readonly object sync = new object();
private readonly StreamWriter writer;
public ConsoleFileLogger(string path)
{
writer = new StreamWriter(path, false, new UTF8Encoding(false))
{
AutoFlush = true
};
}
public void Info(string message)
{
Write("INFO", message);
}
public void Warning(string message)
{
Write("WARNUNG", message);
}
public void Error(string message)
{
Write("FEHLER", message);
}
public void Dispose()
{
writer.Dispose();
}
private void Write(string level, string message)
{
var line = string.Format(
CultureInfo.InvariantCulture,
"{0:yyyy-MM-dd HH:mm:ss.fff} [{1}] {2}",
DateTime.Now,
level,
message ?? string.Empty);
lock (sync)
{
Console.WriteLine(line);
writer.WriteLine(line);
}
}
}
}
@@ -0,0 +1,68 @@
using System;
using System.Diagnostics;
using BizTalkSapEnvironmentInventory.Models;
namespace BizTalkSapEnvironmentInventory.Infrastructure
{
/// <summary>
/// Isoliert Collector-Fehler und überführt Laufzeit, Status und Fehler in den Bericht.
/// </summary>
internal sealed class SafeCollector
{
private readonly InventoryDocument document;
private readonly ConsoleFileLogger logger;
public SafeCollector(InventoryDocument document, ConsoleFileLogger logger)
{
this.document = document;
this.logger = logger;
}
/// <summary>
/// Führt einen Abschnitt aus, ohne nachfolgende Abschnitte bei einem Fehler zu unterdrücken.
/// </summary>
public void Execute(string name, bool required, Action action)
{
var stopwatch = Stopwatch.StartNew();
logger.Info("Starte Abschnitt: " + name);
try
{
action();
stopwatch.Stop();
document.SectionStatuses.Add(new SectionStatus
{
Name = name,
Status = "Erfolgreich",
Message = "Abschnitt vollständig ausgeführt.",
Required = required,
DurationMilliseconds = stopwatch.ElapsedMilliseconds
});
logger.Info(string.Format(
"Abschnitt abgeschlossen: {0} ({1} ms)",
name,
stopwatch.ElapsedMilliseconds));
}
catch (Exception exception)
{
stopwatch.Stop();
document.SectionStatuses.Add(new SectionStatus
{
Name = name,
Status = required ? "Fehler" : "Teilweise",
Message = exception.GetType().Name + ": " + exception.Message,
Required = required,
DurationMilliseconds = stopwatch.ElapsedMilliseconds
});
document.Findings.Add(new Finding
{
Severity = required ? "Fehler" : "Warnung",
Area = name,
Message = "Datenerfassung fehlgeschlagen: " + exception.Message,
RecommendedAction = "Logdatei, Berechtigungen und lokale BizTalk-Konfiguration prüfen.",
Owner = "BizTalk-Betrieb"
});
logger.Error(name + ": " + exception.Message);
}
}
}
}
@@ -0,0 +1,342 @@
using System;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using BizTalkSapEnvironmentInventory.Collectors;
using BizTalkSapEnvironmentInventory.Configuration;
using BizTalkSapEnvironmentInventory.Models;
using BizTalkSapEnvironmentInventory.Reporting;
namespace BizTalkSapEnvironmentInventory.Infrastructure
{
/// <summary>
/// Abhängigkeitsfreier Testläufer für Secret-Schutz, SAP-Bindingparser und DOCX-Paket.
/// </summary>
internal static class SelfTestRunner
{
/// <summary>
/// Führt alle sicherheits- und ausgaberelevanten Tests aus.
/// </summary>
public static void Run()
{
TestSanitizer();
TestBindingParser();
TestDocxPackage();
}
/// <summary>
/// Schreibt einen dauerhaften Musterbericht für eine externe Word-/LibreOffice-Prüfung.
/// </summary>
public static void WriteSample(string path)
{
var document = new InventoryDocument
{
EnvironmentName = "ACC-MUSTER",
ComputerName = "BIZTALK-ACC-01",
ToolVersion = "1.0.0.0",
StartedUtc = DateTime.UtcNow.AddSeconds(-3),
CompletedUtc = DateTime.UtcNow
};
document.System.Properties.Add(
new NameValueRecord("BizTalk Produktversion", "3.13.x (Muster)"));
document.System.Properties.Add(
new NameValueRecord("BizTalk Management SQL Server", "SQL-ACC-01"));
document.Applications.Add(new ApplicationRecord
{
Name = "MasterData_SAP_WebGIS",
Status = "Gestartet"
});
var endpoint = new SapEndpointRecord
{
ApplicationName = "MasterData_SAP_WebGIS",
Direction = "Senden",
Name = "Send_SAP_WebGIS",
ParentPortName = "Send_SAP_WebGIS",
AdapterName = "WCF-SAP",
HostName = "Snd_SAP_ERP_C1_64_B",
Status = "Gestartet",
Address = "sap://Client=100;lang=DE@A/sap-acc.example.local/00",
SecurityMode = "SAP SNC",
CredentialReference = "Kennwort wird nicht dokumentiert.",
Source = "Muster"
};
endpoint.Properties.Add(new NameValueRecord("Client", "100", "SAP-URI"));
endpoint.Properties.Add(new NameValueRecord("GatewayHost", "sap-gw-acc", "SAP-URI"));
endpoint.Properties.Add(new NameValueRecord("UseSnc", "true", "Binding"));
endpoint.Properties.Add(new NameValueRecord("SncLibrary", @"C:\SAP\sapcrypto.dll", "Binding"));
endpoint.Operations.Add("http://Microsoft.LobServices.Sap/2007/03/Idoc/3/ORDERS05");
document.SapEndpoints.Add(endpoint);
document.SectionStatuses.Add(new SectionStatus
{
Name = "Mustererfassung",
Status = "Erfolgreich",
Message = "Musterbericht",
Required = true,
DurationMilliseconds = 42
});
document.Findings.Add(new Finding
{
Severity = "Offen",
Area = "SAP WE20",
Message = "Partnerprofil muss aus SAP ergänzt werden.",
RecommendedAction = "WE20-Export beifügen.",
Owner = "SAP Basis"
});
new DocxReportWriter().Write(document, Path.GetFullPath(path));
}
private static void TestSanitizer()
{
var source = XElement.Parse(
"<configuration password=\"clear\"><add token=\"abc\" />"
+ "<EncryptedData><CipherValue>blob</CipherValue></EncryptedData></configuration>");
var result = SensitiveDataSanitizer.SanitizeXml(source).ToString();
Assert(!result.Contains("clear"), "XML-Sanitizer ließ Kennwort stehen.");
Assert(!result.Contains("abc"), "XML-Sanitizer ließ Token stehen.");
Assert(!result.Contains("blob"), "XML-Sanitizer ließ Ciphertext stehen.");
var uri = SensitiveDataSanitizer.SanitizeText(
"sap://Client=100;Password=secret@A/sap.example/00?token=abc");
Assert(!uri.Contains("secret"), "URI-Sanitizer ließ Kennwort stehen.");
Assert(!uri.Contains("abc"), "URI-Sanitizer ließ Token stehen.");
var embeddedXml = SensitiveDataSanitizer.SanitizeText(
"<binding password=\"xmlsecret\" token='xmltoken' />");
Assert(!embeddedXml.Contains("xmlsecret"), "Text-Sanitizer ließ XML-Kennwort stehen.");
Assert(!embeddedXml.Contains("xmltoken"), "Text-Sanitizer ließ XML-Token stehen.");
}
private static void TestBindingParser()
{
var directory = CreateTemporaryDirectory();
var logPath = Path.Combine(directory, "test.log");
try
{
var document = new InventoryDocument
{
EnvironmentName = "TEST",
ComputerName = "BIZTALK-TEST",
StartedUtc = DateTime.UtcNow
};
var options = CommandLineOptions.Parse(new[]
{
"--environment", "TEST",
"--output", directory
});
using (var logger = new ConsoleFileLogger(logPath))
{
var collector = new BindingExportCollector(options, document, logger, 30);
collector.ParseBindingDocument(CreateSampleBinding(), "Self-Test");
}
Assert(document.SapEndpoints.Count == 2, "Nicht alle SAP-Endpunkte wurden erkannt.");
var endpoint = document.SapEndpoints.Single(item => item.Direction == "Senden");
Assert(endpoint.GetProperty("Client") == "100", "SAP Client wurde nicht aus URI gelesen.");
Assert(
endpoint.GetProperty("ApplicationServerHost") == "sap.example.local",
"SAP Application Server wurde nicht aus URI gelesen.");
Assert(endpoint.GetProperty("UseSnc") == "true", "UseSnc wurde nicht gelesen.");
Assert(
endpoint.Properties.All(item => !item.Value.Contains("supersecret")),
"Binding-Parser ließ ein Kennwort stehen.");
Assert(endpoint.SecurityMode == "SAP SNC", "SNC-Modus wurde nicht erkannt.");
Assert(endpoint.ApplicationName == "SAP_Order", "Anwendungszuordnung fehlt.");
var receive = document.SapEndpoints.Single(item => item.Direction == "Empfangen");
Assert(receive.AdapterName == "WCF-Custom", "WCF-Custom SAP-Endpunkt fehlt.");
Assert(
receive.GetProperty("ListenerProgramId") == "BIZTALK_TEST",
"Listener Program ID wurde nicht aus URI gelesen.");
Assert(
receive.GetProperty("DestinationName") == "RFC_TEST",
"Destination Name wurde nicht aus URI gelesen.");
}
finally
{
DeleteDirectory(directory);
}
}
private static void TestDocxPackage()
{
var directory = CreateTemporaryDirectory();
var path = Path.Combine(directory, "self-test.docx");
try
{
var document = new InventoryDocument
{
EnvironmentName = "<ACC&TEST>",
ComputerName = "BIZTALK&01",
ToolVersion = "1.0.0.0",
StartedUtc = DateTime.UtcNow.AddSeconds(-1),
CompletedUtc = DateTime.UtcNow
};
document.Applications.Add(new ApplicationRecord
{
Name = "SAP_Order",
Status = "Gestartet"
});
document.SapEndpoints.Add(new SapEndpointRecord
{
ApplicationName = "SAP_Order",
Direction = "Senden",
Name = "SAP<Send>",
AdapterName = "WCF-SAP",
Address = "sap://Client=100@A/sap.example.local/00",
SecurityMode = "SAP SNC",
CredentialReference = "Kein Kennwort",
Source = "Self-Test"
});
document.SapEndpoints[0].Properties.Add(
new NameValueRecord("Password", "supersecret", "Self-Test"));
new DocxReportWriter().Write(document, path);
using (var archive = ZipFile.OpenRead(path))
{
foreach (var required in new[]
{
"[Content_Types].xml",
"_rels/.rels",
"word/document.xml",
"word/styles.xml",
"word/_rels/document.xml.rels",
"docProps/core.xml",
"docProps/app.xml"
})
{
Assert(archive.GetEntry(required) != null, "DOCX-Part fehlt: " + required);
}
foreach (var entry in archive.Entries.Where(item =>
item.FullName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase)
|| item.FullName.EndsWith(".rels", StringComparison.OrdinalIgnoreCase)))
{
using (var stream = entry.Open())
{
XDocument.Load(stream);
}
}
AssertPackageNamespaces(archive);
using (var stream = archive.GetEntry("word/document.xml").Open())
using (var reader = new StreamReader(stream, Encoding.UTF8))
{
var xml = reader.ReadToEnd();
Assert(!xml.Contains("supersecret"), "DOCX enthält Testkennwort.");
Assert(!xml.Contains("<ACC&TEST>"), "Text wurde nicht XML-sicher geschrieben.");
}
}
}
finally
{
DeleteDirectory(directory);
}
}
private static void AssertPackageNamespaces(ZipArchive archive)
{
var contentTypes = LoadEntryXml(archive, "[Content_Types].xml");
var contentTypeNamespace = (XNamespace)
"http://schemas.openxmlformats.org/package/2006/content-types";
Assert(
contentTypes.Root.Elements(contentTypeNamespace + "Override").Any(),
"Content-Type-Overrides liegen nicht im OPC-Namespace.");
var relationships = LoadEntryXml(archive, "_rels/.rels");
var relationshipNamespace = (XNamespace)
"http://schemas.openxmlformats.org/package/2006/relationships";
Assert(
relationships.Root.Elements(relationshipNamespace + "Relationship").Count() == 3,
"Paketbeziehungen liegen nicht vollständig im OPC-Namespace.");
}
private static XDocument LoadEntryXml(ZipArchive archive, string entryName)
{
using (var stream = archive.GetEntry(entryName).Open())
{
return XDocument.Load(stream);
}
}
private static XDocument CreateSampleBinding()
{
return XDocument.Parse(
@"<BindingInfo>
<ModuleRefCollection>
<ModuleRef Name=""[Application:SAP_Order]"" />
</ModuleRefCollection>
<SendPortCollection>
<SendPort Name=""Send_SAP_Order"">
<ApplicationName>SAP_Order</ApplicationName>
<PrimaryTransport>
<Address>sap://Client=100;lang=DE@A/sap.example.local/00?GwHost=gateway.example.local&amp;GwServ=sapgw00</Address>
<TransportType Name=""WCF-SAP"" />
<SendHandler Name=""Snd_SAP_ERP_C1_64_B"" />
<TransportTypeData>&lt;CustomProps&gt;
&lt;BindingType vt=""8""&gt;sapBinding&lt;/BindingType&gt;
&lt;BindingConfiguration vt=""8""&gt;&amp;lt;binding name=""sapBinding"" useSnc=""true"" sncLibrary=""C:\SAP\sapcrypto.dll"" sncPartnerName=""p:SAP/ERP"" /&amp;gt;&lt;/BindingConfiguration&gt;
&lt;Password vt=""8""&gt;supersecret&lt;/Password&gt;
&lt;Action vt=""8""&gt;http://Microsoft.LobServices.Sap/2007/03/Idoc/3/ORDERS05&lt;/Action&gt;
&lt;/CustomProps&gt;</TransportTypeData>
</PrimaryTransport>
</SendPort>
</SendPortCollection>
<ReceivePortCollection>
<ReceivePort Name=""Receive_SAP_Order"">
<ApplicationName>SAP_Order</ApplicationName>
<ReceiveLocations>
<ReceiveLocation Name=""ReceiveLocation_SAP_Order"" Enable=""true"">
<Address>sap://Client=100;lang=DE@D/RFC_TEST?ListenerGwHost=gateway.example.local&amp;ListenerGwServ=sapgw00&amp;ListenerProgramId=BIZTALK_TEST</Address>
<ReceiveLocationTransportType Name=""WCF-Custom"" />
<ReceiveHandler Name=""Rcv_SAP_ERP_C1_64_B"" />
<ReceiveLocationTransportTypeData>
<CustomProps>
<BindingType vt=""8"">sapBinding</BindingType>
<BindingConfiguration vt=""8"">&lt;binding name=""sapBinding"" useSnc=""true"" sncLibrary=""C:\SAP\sapcrypto.dll"" sncPartnerName=""p:SAP/ERP"" /&gt;</BindingConfiguration>
</CustomProps>
</ReceiveLocationTransportTypeData>
</ReceiveLocation>
</ReceiveLocations>
</ReceivePort>
</ReceivePortCollection>
</BindingInfo>");
}
private static string CreateTemporaryDirectory()
{
var path = Path.Combine(
Path.GetTempPath(),
"BizTalkSapInventoryTests-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(path);
return path;
}
private static void DeleteDirectory(string path)
{
try
{
if (Directory.Exists(path))
{
Directory.Delete(path, true);
}
}
catch (IOException)
{
}
catch (UnauthorizedAccessException)
{
}
}
private static void Assert(bool condition, string message)
{
if (!condition)
{
throw new InvalidOperationException(message);
}
}
}
}
@@ -0,0 +1,125 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Xml.Linq;
namespace BizTalkSapEnvironmentInventory.Infrastructure
{
/// <summary>
/// Zentraler Schutz gegen die Aufnahme von Kennwörtern, Tokens und Schlüsseldaten in Log oder DOCX.
/// </summary>
internal static class SensitiveDataSanitizer
{
private static readonly string[] SensitiveFragments =
{
"password", "passwd", "passphrase", "pwd", "secret", "token",
"privatekey", "clientsecret", "accesskey", "apikey", "connectionstring"
};
private static readonly Regex UriSecretPattern = new Regex(
@"(?i)(password|passwd|passphrase|pwd|secret|token|clientsecret)\s*=\s*(?:""[^""]*""|'[^']*'|[^;&\s""']+)",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex UserInfoPattern = new Regex(
@"(?i)([a-z][a-z0-9+.-]*://)([^/@:\s]+):([^/@\s]+)@",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
/// <summary>
/// Erkennt sensitive Feldnamen unabhängig von Großschreibung und Trennzeichen.
/// </summary>
public static bool IsSensitiveName(string name)
{
var normalized = (name ?? string.Empty).Replace("_", string.Empty)
.Replace("-", string.Empty);
return SensitiveFragments.Any(fragment =>
normalized.IndexOf(fragment, StringComparison.OrdinalIgnoreCase) >= 0);
}
/// <summary>
/// Redigiert einen Wert anhand seines semantischen Feldnamens.
/// </summary>
public static string RedactValue(string name, string value)
{
if (IsSensitiveName(name))
{
return string.IsNullOrWhiteSpace(value)
? "Nicht gesetzt"
: "Konfiguriert (Wert nicht dokumentiert)";
}
return SanitizeText(value);
}
/// <summary>
/// Entfernt Secrets aus URI-/Key-Value-Texten und eingebetteten Benutzerinformationen.
/// </summary>
public static string SanitizeText(string value)
{
if (string.IsNullOrEmpty(value))
{
return string.Empty;
}
var sanitized = UriSecretPattern.Replace(
value,
match => match.Groups[1].Value + "=***REDACTED***");
sanitized = UserInfoPattern.Replace(
sanitized,
match => match.Groups[1].Value + match.Groups[2].Value + ":***REDACTED***@");
return sanitized;
}
/// <summary>
/// Erstellt eine secret-bereinigte Kopie eines XML-Teilbaums.
/// </summary>
public static XElement SanitizeXml(XElement source)
{
if (source == null)
{
return null;
}
if (IsSensitiveName(source.Name.LocalName)
|| IsCipherElement(source.Name.LocalName))
{
return new XElement(source.Name, "***REDACTED***");
}
var result = new XElement(source.Name);
foreach (var attribute in source.Attributes())
{
result.Add(new XAttribute(
attribute.Name,
IsSensitiveName(attribute.Name.LocalName)
? "***REDACTED***"
: SanitizeText(attribute.Value)));
}
foreach (var node in source.Nodes())
{
var element = node as XElement;
if (element != null)
{
result.Add(SanitizeXml(element));
continue;
}
var text = node as XText;
if (text != null)
{
result.Add(new XText(SanitizeText(text.Value)));
}
}
return result;
}
private static bool IsCipherElement(string name)
{
return string.Equals(name, "EncryptedData", StringComparison.OrdinalIgnoreCase)
|| string.Equals(name, "CipherData", StringComparison.OrdinalIgnoreCase)
|| string.Equals(name, "CipherValue", StringComparison.OrdinalIgnoreCase);
}
}
}
@@ -0,0 +1,202 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace BizTalkSapEnvironmentInventory.Models
{
/// <summary>
/// Zentrales, ausschließlich secret-bereinigtes Berichtsmodell aller Collector-Ergebnisse.
/// </summary>
internal sealed class InventoryDocument
{
public InventoryDocument()
{
System = new SystemInventory();
Applications = new List<ApplicationRecord>();
SapEndpoints = new List<SapEndpointRecord>();
Adapters = new List<ComponentRecord>();
Hosts = new List<ComponentRecord>();
RuntimeComponents = new List<ComponentRecord>();
SecurityMaterials = new List<SecurityMaterialRecord>();
Findings = new List<Finding>();
SectionStatuses = new List<SectionStatus>();
}
public string EnvironmentName { get; set; }
public string ComputerName { get; set; }
public string ToolVersion { get; set; }
public DateTime StartedUtc { get; set; }
public DateTime CompletedUtc { get; set; }
public SystemInventory System { get; private set; }
public List<ApplicationRecord> Applications { get; private set; }
public List<SapEndpointRecord> SapEndpoints { get; private set; }
public List<ComponentRecord> Adapters { get; private set; }
public List<ComponentRecord> Hosts { get; private set; }
public List<ComponentRecord> RuntimeComponents { get; private set; }
public List<SecurityMaterialRecord> SecurityMaterials { get; private set; }
public List<Finding> Findings { get; private set; }
public List<SectionStatus> SectionStatuses { get; private set; }
public bool HasRequiredSectionFailure
{
get
{
return SectionStatuses.Any(item =>
item.Required
&& !string.Equals(item.Status, "Erfolgreich", StringComparison.OrdinalIgnoreCase))
|| Findings.Any(item =>
string.Equals(item.Severity, "Fehler", StringComparison.OrdinalIgnoreCase));
}
}
}
internal sealed class SystemInventory
{
public SystemInventory()
{
Properties = new List<NameValueRecord>();
}
public string BizTalkInstallPath { get; set; }
public string ManagementServer { get; set; }
public string ManagementDatabase { get; set; }
public List<NameValueRecord> Properties { get; private set; }
}
internal sealed class ApplicationRecord
{
public ApplicationRecord()
{
Artifacts = new List<ArtifactRecord>();
}
public string Name { get; set; }
public string Description { get; set; }
public string Status { get; set; }
public List<ArtifactRecord> Artifacts { get; private set; }
public int Count(string artifactType)
{
return Artifacts.Count(item =>
string.Equals(item.Type, artifactType, StringComparison.OrdinalIgnoreCase));
}
}
internal sealed class ArtifactRecord
{
public ArtifactRecord()
{
Properties = new List<NameValueRecord>();
}
public string Type { get; set; }
public string Name { get; set; }
public string ApplicationName { get; set; }
public string Status { get; set; }
public List<NameValueRecord> Properties { get; private set; }
}
internal sealed class SapEndpointRecord
{
public SapEndpointRecord()
{
Properties = new List<NameValueRecord>();
Operations = new List<string>();
}
public string ApplicationName { get; set; }
public string Direction { get; set; }
public string Name { get; set; }
public string ParentPortName { get; set; }
public string AdapterName { get; set; }
public string HostName { get; set; }
public string Status { get; set; }
public string Address { get; set; }
public string SecurityMode { get; set; }
public string CredentialReference { get; set; }
public string Source { get; set; }
public List<NameValueRecord> Properties { get; private set; }
public List<string> Operations { get; private set; }
public string GetProperty(params string[] names)
{
foreach (var name in names)
{
var match = Properties.FirstOrDefault(item =>
string.Equals(item.Name, name, StringComparison.OrdinalIgnoreCase));
if (match != null)
{
return match.Value;
}
}
return string.Empty;
}
}
internal sealed class ComponentRecord
{
public ComponentRecord()
{
Properties = new List<NameValueRecord>();
}
public string Category { get; set; }
public string Name { get; set; }
public string Status { get; set; }
public List<NameValueRecord> Properties { get; private set; }
}
internal sealed class SecurityMaterialRecord
{
public string Kind { get; set; }
public string Name { get; set; }
public string Location { get; set; }
public string Exists { get; set; }
public string Fingerprint { get; set; }
public string ValidFrom { get; set; }
public string ValidTo { get; set; }
public string Details { get; set; }
}
internal sealed class NameValueRecord
{
public NameValueRecord()
{
}
public NameValueRecord(string name, string value)
: this(name, value, string.Empty)
{
}
public NameValueRecord(string name, string value, string source)
{
Name = name ?? string.Empty;
Value = value ?? string.Empty;
Source = source ?? string.Empty;
}
public string Name { get; set; }
public string Value { get; set; }
public string Source { get; set; }
}
internal sealed class Finding
{
public string Severity { get; set; }
public string Area { get; set; }
public string Message { get; set; }
public string RecommendedAction { get; set; }
public string Owner { get; set; }
}
internal sealed class SectionStatus
{
public string Name { get; set; }
public string Status { get; set; }
public string Message { get; set; }
public bool Required { get; set; }
public long DurationMilliseconds { get; set; }
}
}
@@ -0,0 +1,303 @@
using System;
using System.Configuration;
using System.Globalization;
using System.IO;
using System.Linq;
using BizTalkSapEnvironmentInventory.Collectors;
using BizTalkSapEnvironmentInventory.Configuration;
using BizTalkSapEnvironmentInventory.Infrastructure;
using BizTalkSapEnvironmentInventory.Models;
using BizTalkSapEnvironmentInventory.Reporting;
namespace BizTalkSapEnvironmentInventory
{
/// <summary>
/// Orchestriert die read-only Erfassung, Fortschrittsausgabe und atomare DOCX-Erzeugung.
/// </summary>
internal static class Program
{
/// <summary>
/// Programmeinstieg mit den dokumentierten Exitcodes 0, 1 und 2.
/// </summary>
private static int Main(string[] args)
{
CommandLineOptions options;
try
{
options = CommandLineOptions.Parse(args);
}
catch (Exception exception)
{
Console.Error.WriteLine("FEHLER: " + exception.Message);
Console.Error.WriteLine();
Console.Error.WriteLine(CommandLineOptions.Usage());
return 2;
}
if (options.ShowHelp)
{
Console.WriteLine(CommandLineOptions.Usage());
return 0;
}
if (options.SelfTest)
{
return RunSelfTest();
}
try
{
Directory.CreateDirectory(options.OutputDirectory);
}
catch (Exception exception) when (
exception is IOException
|| exception is UnauthorizedAccessException
|| exception is ArgumentException)
{
Console.Error.WriteLine("FEHLER: Ausgabeordner kann nicht erstellt werden: " + exception.Message);
return 2;
}
var timestamp = DateTime.Now.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture);
var safeMachine = SanitizeFileName(Environment.MachineName);
var baseName = "BizTalk-SAP-Dokumentation-"
+ options.EnvironmentName
+ "-"
+ safeMachine
+ "-"
+ timestamp;
var logPath = Path.Combine(options.OutputDirectory, baseName + ".log");
var reportPath = Path.Combine(options.OutputDirectory, baseName + ".docx");
using (var logger = new ConsoleFileLogger(logPath))
{
var document = new InventoryDocument
{
EnvironmentName = options.EnvironmentName,
ComputerName = Environment.MachineName,
StartedUtc = DateTime.UtcNow
};
logger.Info("BEW BizTalk SAP Environment Inventory startet.");
logger.Info("Umgebung: " + options.EnvironmentName);
logger.Info("Server: " + Environment.MachineName);
logger.Info("Ausgabeordner: " + options.OutputDirectory);
logger.Info("Modus: read-only");
var safeCollector = new SafeCollector(document, logger);
safeCollector.Execute(
"System und BizTalk-Installation",
true,
() => new SystemCollector(options).Collect(document));
safeCollector.Execute(
"BizTalk-Anwendungen und WMI-Artefakte",
true,
() => new BizTalkWmiCollector(
document,
logger,
ReadIntSetting("WmiTimeoutSeconds", 30),
ReadIntSetting("MaxArtifactDetailsPerType", 5000)).Collect());
safeCollector.Execute(
"SAP-Bindingparameter",
true,
() => new BindingExportCollector(
options,
document,
logger,
ReadIntSetting("ProcessTimeoutSeconds", 120)).Collect());
safeCollector.Execute(
"SAP NCo, SNC und Zertifikatsmetadaten",
false,
() => new SapRuntimeCollector(
document,
logger,
ReadBoolSetting("IncludeSapRelatedLocalMachineCertificates", true)).Collect());
EvaluateCompleteness(document, ReadBoolSetting("ExpectedUseSnc", true));
document.CompletedUtc = DateTime.UtcNow;
try
{
logger.Info("Erzeuge Microsoft-Word-Dokument: " + reportPath);
new DocxReportWriter().Write(document, reportPath);
logger.Info("Word-Dokument erfolgreich erzeugt: " + reportPath);
logger.Info("Logdatei: " + logPath);
}
catch (Exception exception)
{
logger.Error("Word-Dokument konnte nicht erzeugt werden: " + exception);
return 2;
}
var exitCode = document.HasRequiredSectionFailure ? 1 : 0;
logger.Info("Erfassung beendet. Exitcode: " + exitCode);
return exitCode;
}
}
private static int RunSelfTest()
{
try
{
Console.WriteLine("Starte Sicherheits-, Binding- und DOCX-Self-Test ...");
SelfTestRunner.Run();
Console.WriteLine("Self-Test erfolgreich.");
return 0;
}
catch (Exception exception)
{
Console.Error.WriteLine("Self-Test fehlgeschlagen: " + exception);
return 1;
}
}
private static void EvaluateCompleteness(InventoryDocument document, bool expectedUseSnc)
{
if (document.Applications.Count == 0)
{
document.Findings.Add(new Finding
{
Severity = "Fehler",
Area = "BizTalk-Anwendungen",
Message = "Keine BizTalk-Anwendungen wurden ermittelt.",
RecommendedAction = "Mit einem Mitglied der BizTalk Server Administrators oder Operators lokal erneut ausführen.",
Owner = "BizTalk-Betrieb"
});
}
if (document.SapEndpoints.Count == 0)
{
document.Findings.Add(new Finding
{
Severity = "Fehler",
Area = "SAP-Endpunkte",
Message = "Keine WCF-SAP-/SAP-Endpunkte wurden erkannt.",
RecommendedAction = "BTSTask-Gruppenexport, WCF-Custom sapBinding und WCF-SAP-Adapterdefinition prüfen.",
Owner = "BizTalk-Betrieb"
});
}
foreach (var endpoint in document.SapEndpoints)
{
if (string.IsNullOrWhiteSpace(endpoint.ApplicationName))
{
document.Findings.Add(new Finding
{
Severity = "Warnung",
Area = "Anwendungszuordnung",
Message = "SAP-Endpunkt " + endpoint.Name + " konnte keiner BizTalk-Anwendung zugeordnet werden.",
RecommendedAction = "Port in der BizTalk Administration Console prüfen und Zuordnung ergänzen.",
Owner = "BizTalk-Betrieb"
});
}
var useSnc = endpoint.GetProperty("UseSnc", "UseSNC");
if (expectedUseSnc && !IsTrue(useSnc))
{
document.Findings.Add(new Finding
{
Severity = "Warnung",
Area = "SNC",
Message = "Für SAP-Endpunkt " + endpoint.Name
+ " ist UseSnc nicht eindeutig true (Wert: "
+ (string.IsNullOrWhiteSpace(useSnc) ? "nicht ermittelt" : useSnc)
+ ").",
RecommendedAction = "WCF-SAP-Binding und tatsächlichen SNC-Verbindungsaufbau prüfen.",
Owner = "BizTalk-Betrieb/SAP Basis"
});
}
if (IsTrue(useSnc)
&& string.IsNullOrWhiteSpace(endpoint.GetProperty(
"SncLibrary",
"sncLibrary",
"SncLibraryPath")))
{
document.Findings.Add(new Finding
{
Severity = "Warnung",
Area = "SNC",
Message = "SNC ist für " + endpoint.Name
+ " aktiv, aber die SNC-Bibliothek wurde nicht eindeutig aus dem Binding gelesen.",
RecommendedAction = "SncLibrary, Prozessarchitektur und Bibliotheksablage auf dem BizTalk Server bestätigen.",
Owner = "BizTalk-Betrieb/Security"
});
}
}
AddManualEvidenceFindings(document);
}
private static void AddManualEvidenceFindings(InventoryDocument document)
{
document.Findings.Add(new Finding
{
Severity = "Offen",
Area = "SAP WE20",
Message = "Partnerprofile (WE20) liegen im SAP-System und sind lokal nicht vollständig auslesbar.",
RecommendedAction = "Partner, Nachrichtentyp, Basistyp/Erweiterung und Empfängerport aus SAP exportieren.",
Owner = "SAP Basis/Fachteam"
});
document.Findings.Add(new Finding
{
Severity = "Offen",
Area = "SAP WE21",
Message = "Portdefinitionen (WE21) liegen im SAP-System und sind lokal nicht vollständig auslesbar.",
RecommendedAction = "Portname, RFC-Destination, IDoc-Version und WE20-Zuordnung aus SAP ergänzen.",
Owner = "SAP Basis"
});
document.Findings.Add(new Finding
{
Severity = "Offen",
Area = "Credential-/SNC-Rotation",
Message = "Ablage, Verantwortlicher und Rotationsprozess für SAP-Credentials bzw. SNC-PSE/Zertifikat sind keine technische Bindingeigenschaft.",
RecommendedAction = "Security-Runbook mit Ablaufdatum, Rotation, Berechtigungen und Recovery referenzieren.",
Owner = "SAP Basis/Security"
});
document.Findings.Add(new Finding
{
Severity = "Offen",
Area = "Disaster Recovery",
Message = "Ob die RFC-Destination SAP-seitig für Frankfurt umgeschaltet wird, ist vom BizTalk Server nicht feststellbar.",
RecommendedAction = "Umschalt- und Rückschaltverfahren mit SAP Basis klären und testen.",
Owner = "SAP Basis/DR-Verantwortliche"
});
}
private static int ReadIntSetting(string name, int defaultValue)
{
int parsed;
return int.TryParse(
ConfigurationManager.AppSettings[name],
NumberStyles.Integer,
CultureInfo.InvariantCulture,
out parsed)
? parsed
: defaultValue;
}
private static bool ReadBoolSetting(string name, bool defaultValue)
{
bool parsed;
return bool.TryParse(ConfigurationManager.AppSettings[name], out parsed)
? parsed
: defaultValue;
}
private static bool IsTrue(string value)
{
return string.Equals(value, "true", StringComparison.OrdinalIgnoreCase)
|| string.Equals(value, "1", StringComparison.OrdinalIgnoreCase)
|| string.Equals(value, "yes", StringComparison.OrdinalIgnoreCase)
|| string.Equals(value, "ja", StringComparison.OrdinalIgnoreCase);
}
private static string SanitizeFileName(string value)
{
var invalid = Path.GetInvalidFileNameChars();
return new string((value ?? "UNKNOWN")
.Select(character => invalid.Contains(character) ? '-' : character)
.ToArray());
}
}
}
@@ -0,0 +1,14 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("BEW BizTalk SAP Environment Inventory")]
[assembly: AssemblyDescription("Read-only BizTalk 2020 SAP adapter and application inventory")]
[assembly: AssemblyCompany("JR IT Services")]
[assembly: AssemblyProduct("BEW BizTalk SAP Environment Inventory")]
[assembly: ComVisible(false)]
[assembly: Guid("ae86a6ce-17c1-4c10-925e-e25f25f1b56e")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: InternalsVisibleTo("BizTalkSapEnvironmentInventory.Tests")]
@@ -0,0 +1,934 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Xml;
using BizTalkSapEnvironmentInventory.Infrastructure;
using BizTalkSapEnvironmentInventory.Models;
namespace BizTalkSapEnvironmentInventory.Reporting
{
/// <summary>
/// Erzeugt ein Word-kompatibles Office-Open-XML-Paket ohne Office-Interop.
/// </summary>
internal sealed class DocxReportWriter
{
private const string WordNamespace =
"http://schemas.openxmlformats.org/wordprocessingml/2006/main";
private const string RelationshipNamespace =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships";
private const string PackageRelationshipNamespace =
"http://schemas.openxmlformats.org/package/2006/relationships";
private const string ContentTypeNamespace =
"http://schemas.openxmlformats.org/package/2006/content-types";
private const string ExtendedPropertiesNamespace =
"http://schemas.openxmlformats.org/officeDocument/2006/extended-properties";
/// <summary>
/// Schreibt den Bericht zunächst temporär und verschiebt ihn anschließend atomar an das Ziel.
/// </summary>
public void Write(InventoryDocument document, string outputPath)
{
var directory = Path.GetDirectoryName(outputPath);
if (string.IsNullOrWhiteSpace(directory))
{
throw new ArgumentException("Ausgabepfad besitzt kein Verzeichnis.", "outputPath");
}
Directory.CreateDirectory(directory);
var temporaryPath = Path.Combine(
directory,
"." + Path.GetFileName(outputPath) + "." + Guid.NewGuid().ToString("N") + ".tmp");
try
{
using (var archive = ZipFile.Open(temporaryPath, ZipArchiveMode.Create))
{
WriteContentTypes(archive);
WritePackageRelationships(archive);
WriteDocumentRelationships(archive);
WriteStyles(archive);
WriteCoreProperties(archive, document);
WriteApplicationProperties(archive);
WriteMainDocument(archive, document);
}
if (File.Exists(outputPath))
{
File.Replace(temporaryPath, outputPath, null);
}
else
{
File.Move(temporaryPath, outputPath);
}
}
finally
{
if (File.Exists(temporaryPath))
{
File.Delete(temporaryPath);
}
}
}
private static void WriteMainDocument(ZipArchive archive, InventoryDocument document)
{
using (var writer = CreateXmlWriter(archive, "word/document.xml"))
{
writer.WriteStartDocument();
writer.WriteStartElement("w", "document", WordNamespace);
writer.WriteAttributeString("xmlns", "r", null, RelationshipNamespace);
writer.WriteStartElement("w", "body", WordNamespace);
WriteTitle(writer, "BizTalk SAP Environment Inventory");
WriteSubtitle(
writer,
"Umgebung " + Safe(document.EnvironmentName)
+ " " + Safe(document.ComputerName));
WriteParagraph(
writer,
"Erstellt am "
+ document.CompletedUtc.ToLocalTime().ToString(
"yyyy-MM-dd HH:mm:ss",
CultureInfo.InvariantCulture)
+ " mit Toolversion "
+ Safe(document.ToolVersion)
+ ".",
"Normal",
false,
"666666");
WriteParagraph(
writer,
"VERTRAULICHE BETRIEBSDOKUMENTATION enthält interne System-, Endpunkt-, "
+ "Host-, Zertifikats- und Dateipfadinformationen. Keine Kennwörter oder privaten Schlüssel.",
"IntenseQuote",
true,
"9C0006");
WriteHeading(writer, "1. Ziel und Erfassungsumfang", 1);
WriteParagraph(
writer,
"Der Bericht dokumentiert die lokal sichtbare BizTalk-Server-2020-Konfiguration "
+ "für SAP-Adapterverbindungen und stellt die vollständige Liste der BizTalk-Anwendungen "
+ "bereit. ACC und PROD bestehen jeweils aus genau einem BizTalk Server und einem "
+ "separaten SQL Server. Das Tool wird lokal auf dem jeweiligen BizTalk Server ausgeführt.",
"Normal",
false,
null);
WriteBullet(writer, "read-only WMI-Abfragen im Namespace root\\MicrosoftBizTalkServer");
WriteBullet(writer, "temporärer BTSTask-Gruppenbindingexport zur Auswertung physischer Ports");
WriteBullet(writer, "Ermittlung von WCF-SAP-, SAP-NCo- und SNC-Metadaten");
WriteBullet(writer, "Word-kompatibles DOCX ohne Microsoft Office auf dem Server");
WriteBullet(writer, "keine Remoteänderungen, kein SAP-Login und kein Export geheimer Werte");
WriteHeading(writer, "2. Management Summary", 1);
WriteTable(
writer,
new[] { "Kennzahl", "Wert" },
new[]
{
Row("Umgebung", document.EnvironmentName),
Row("Untersuchter BizTalk Server", document.ComputerName),
Row("BizTalk-Anwendungen", document.Applications.Count.ToString(CultureInfo.InvariantCulture)),
Row("SAP-Endpunkte", document.SapEndpoints.Count.ToString(CultureInfo.InvariantCulture)),
Row("SAP-Adapterdefinitionen", document.Adapters.Count.ToString(CultureInfo.InvariantCulture)),
Row("Host-/Handler-Datensätze", document.Hosts.Count.ToString(CultureInfo.InvariantCulture)),
Row("Runtime-Komponenten", document.RuntimeComponents.Count.ToString(CultureInfo.InvariantCulture)),
Row("Security-Materialien", document.SecurityMaterials.Count.ToString(CultureInfo.InvariantCulture)),
Row("Findings", document.Findings.Count.ToString(CultureInfo.InvariantCulture))
});
WriteHeading(writer, "3. Erfassungsstatus", 1);
WriteTable(
writer,
new[] { "Abschnitt", "Pflicht", "Status", "Dauer (ms)", "Meldung" },
document.SectionStatuses.Select(item => new[]
{
item.Name,
item.Required ? "Ja" : "Nein",
item.Status,
item.DurationMilliseconds.ToString(CultureInfo.InvariantCulture),
item.Message
}));
WriteHeading(writer, "4. System und BizTalk-Gruppe", 1);
WriteTable(
writer,
new[] { "Eigenschaft", "Wert", "Quelle" },
document.System.Properties.Select(item => new[]
{
item.Name,
item.Value,
item.Source
}));
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.",
"Normal",
false,
null);
WriteTable(
writer,
new[]
{
"Anwendung", "Status", "Orchestrierungen", "Send Ports", "Receive Ports",
"Receive Locations", "Assemblies", "Schemas", "Maps", "Pipelines"
},
document.Applications.Select(item => new[]
{
item.Name,
item.Status,
Count(item, "Orchestration"),
Count(item, "SendPort"),
Count(item, "ReceivePort"),
Count(item, "ReceiveLocation"),
Count(item, "Assembly"),
Count(item, "Schema"),
Count(item, "Map"),
Count(item, "Pipeline")
}));
WriteHeading(writer, "6. SAP-Endpunkte und Verbindungsparameter", 1);
if (document.SapEndpoints.Count == 0)
{
WriteParagraph(
writer,
"Es wurden keine SAP-Endpunkte erkannt. Das ist für die bekannte Umgebung unerwartet; "
+ "Binding-Export, Adaptername WCF-SAP/WCF-Custom und Berechtigungen prüfen.",
"IntenseQuote",
true,
"9C0006");
}
else
{
WriteTable(
writer,
new[]
{
"Anwendung", "Richtung", "Endpunkt", "Adapter",
"Host/Handler", "Security", "Quelle"
},
document.SapEndpoints
.OrderBy(item => item.ApplicationName, StringComparer.OrdinalIgnoreCase)
.ThenBy(item => item.Direction, StringComparer.OrdinalIgnoreCase)
.ThenBy(item => item.Name, StringComparer.OrdinalIgnoreCase)
.Select(item => new[]
{
Empty(item.ApplicationName),
item.Direction,
item.Name,
item.AdapterName,
Empty(item.HostName),
item.SecurityMode,
item.Source
}));
foreach (var endpoint in document.SapEndpoints
.OrderBy(item => item.ApplicationName, StringComparer.OrdinalIgnoreCase)
.ThenBy(item => item.Name, StringComparer.OrdinalIgnoreCase))
{
WriteHeading(
writer,
endpoint.Direction + ": " + Empty(endpoint.Name),
2);
WriteTable(
writer,
new[] { "Eigenschaft", "Wert" },
new[]
{
Row("BizTalk-Anwendung", Empty(endpoint.ApplicationName)),
Row("Übergeordneter Port", Empty(endpoint.ParentPortName)),
Row("Adapter", Empty(endpoint.AdapterName)),
Row("Host/Handler", Empty(endpoint.HostName)),
Row("Status", Empty(endpoint.Status)),
Row("Adresse", Empty(endpoint.Address)),
Row("Security-Modus", Empty(endpoint.SecurityMode)),
Row("Credential-Nachweis", Empty(endpoint.CredentialReference))
});
var orderedProperties = endpoint.Properties
.OrderBy(item => PropertyOrder(item.Name))
.ThenBy(item => item.Name, StringComparer.OrdinalIgnoreCase)
.Select(item => new[]
{
item.Name,
Truncate(
SensitiveDataSanitizer.RedactValue(item.Name, item.Value),
4000),
item.Source
});
WriteTable(
writer,
new[] { "Verbindungs-/Bindingparameter", "Wert", "Quelle" },
orderedProperties);
if (endpoint.Operations.Count > 0)
{
WriteParagraph(writer, "Aktionen/Operationen:", "Normal", true, null);
foreach (var operation in endpoint.Operations)
{
WriteBullet(writer, Truncate(operation, 2000));
}
}
}
}
WriteHeading(writer, "7. SAP-Adapter, Hosts und Handler", 1);
WriteComponentSection(writer, "Adapterdefinitionen", document.Adapters);
WriteComponentSection(writer, "Hosts und Handler", document.Hosts);
WriteHeading(writer, "8. SAP NCo und SNC Runtime", 1);
WriteComponentSection(writer, "Installationen, Dateien und Umgebungsvariablen", document.RuntimeComponents);
WriteHeading(writer, "9. SNC-/Zertifikatsmaterial", 1);
WriteParagraph(
writer,
"Dokumentiert werden ausschließlich Pfad, Vorhandensein, Gültigkeit und Fingerprint. "
+ "Private Schlüssel, PSE-Inhalte und Kennwörter werden nicht exportiert.",
"Normal",
false,
null);
WriteTable(
writer,
new[]
{
"Art", "Name", "Ablage", "Vorhanden", "Fingerprint",
"Gültig ab", "Gültig bis", "Details"
},
document.SecurityMaterials.Select(item => new[]
{
item.Kind,
item.Name,
item.Location,
item.Exists,
item.Fingerprint,
item.ValidFrom,
item.ValidTo,
item.Details
}));
WriteHeading(writer, "10. IDoc-/RFC-Indikatoren", 1);
var sapArtifacts = FindSapArtifacts(document).ToList();
WriteTable(
writer,
new[] { "Anwendung", "Artefakttyp", "Name", "Status/Indikator" },
sapArtifacts.Select(item => new[]
{
item.ApplicationName,
item.Type,
item.Name,
item.Status
}));
if (sapArtifacts.Count == 0)
{
WriteParagraph(
writer,
"Keine eindeutig als SAP/IDoc erkennbaren Schema- oder Artefaktnamen gefunden. "
+ "Das beweist nicht, dass keine IDoc-Verarbeitung existiert; SAP-Basis- und "
+ "Quellprojekt-Nachweis bleibt erforderlich.",
"IntenseQuote",
false,
"9C6500");
}
WriteHeading(writer, "11. Erforderliche SAP-seitige Nachweise", 1);
WriteParagraph(
writer,
"Die folgenden Objekte liegen im SAP-System oder in Betriebsprozessen und können "
+ "ohne SAP-Zugang nicht belastbar vom BizTalk Server ausgelesen werden. Sie sind "
+ "bewusst als offene Nachweise enthalten.",
"Normal",
false,
null);
WriteTable(
writer,
new[] { "Nachweis", "Automatisch aus BizTalk", "Benötigte Ergänzung", "Zuständigkeit" },
new[]
{
new[]
{
"RFC-Destinationen (SAP, z. B. SM59)",
"Name/ListenerDest und Gatewayparameter, soweit im Binding vorhanden",
"SAP-seitige Definition, Zielhost, Verbindungstest, Verantwortlicher",
"SAP Basis"
},
new[]
{
"Partnerprofile WE20",
"Nicht direkt auslesbar",
"Partner, Nachrichtentyp, Basistyp, Erweiterung, Empfängerport und Verarbeitungsoptionen",
"SAP Basis/Fachteam"
},
new[]
{
"Portdefinitionen WE21",
"Nicht direkt auslesbar",
"Portname, RFC-Destination, IDoc-Version und Zuordnung zu WE20",
"SAP Basis"
},
new[]
{
"IDoc-Basistypen und Erweiterungen",
"Nur Indikatoren aus deployed Schemas und Actions",
"Vollständige SAP-Liste inklusive kundeneigener Erweiterungen bestätigen",
"SAP Basis/Entwicklung"
},
new[]
{
"SAP-Benutzer/Kennwort oder SNC-Identität",
"Modus, Benutzername/SSO-Referenz und SNC-Parameter; kein Kennwort",
"Credential Owner, Ablage, Rotation, Ablauf und Wiederherstellung dokumentieren",
"SAP Basis/Security"
},
new[]
{
"DR-Umschaltung nach Frankfurt",
"Aktuelle RFC-/Gatewayparameter im Binding",
"Klären, ob SAP-seitige RFC-Destination umgeschaltet wird; Runbook und Rückschaltung",
"SAP Basis/DR-Verantwortliche"
}
});
WriteHeading(writer, "12. Findings und offene Punkte", 1);
WriteTable(
writer,
new[] { "Schweregrad", "Bereich", "Feststellung", "Empfehlung", "Owner" },
document.Findings
.OrderBy(item => SeverityOrder(item.Severity))
.ThenBy(item => item.Area, StringComparer.OrdinalIgnoreCase)
.Select(item => new[]
{
item.Severity,
item.Area,
item.Message,
item.RecommendedAction,
item.Owner
}));
WriteHeading(writer, "13. Methodik und Sicherheitsgrenzen", 1);
WriteBullet(writer, "BTSTask-Export ist read-only; temporäre Binding-Datei wird anschließend gelöscht.");
WriteBullet(writer, "BizTalk entfernt Kennwörter beim Binding-Export; das Tool redigiert zusätzlich sensitive Namen und URI-Werte.");
WriteBullet(writer, "DOCX enthält keine privaten Schlüssel, PSE-Inhalte, Kennwörter, Tokens oder entschlüsselbare Secrets.");
WriteBullet(writer, "WMI- und Dateisystemfehler werden abschnittsweise isoliert und als Findings dokumentiert.");
WriteBullet(writer, "Ein Bericht pro Umgebung: lokal auf dem einzelnen BizTalk Server in ACC und PROD ausführen.");
WriteSectionProperties(writer);
writer.WriteEndElement();
writer.WriteEndElement();
writer.WriteEndDocument();
}
}
private static void WriteComponentSection(
XmlWriter writer,
string heading,
IEnumerable<ComponentRecord> components)
{
WriteHeading(writer, heading, 2);
var list = components
.OrderBy(item => item.Category, StringComparer.OrdinalIgnoreCase)
.ThenBy(item => item.Name, StringComparer.OrdinalIgnoreCase)
.ToList();
if (list.Count == 0)
{
WriteParagraph(writer, "Keine Datensätze ermittelt.", "Normal", false, "666666");
return;
}
WriteTable(
writer,
new[] { "Kategorie", "Name", "Status", "Eigenschaften" },
list.Select(item => new[]
{
item.Category,
item.Name,
item.Status,
string.Join(
"; ",
item.Properties.Select(property =>
property.Name + "="
+ SensitiveDataSanitizer.RedactValue(property.Name, property.Value)))
}));
}
private static IEnumerable<ArtifactRecord> FindSapArtifacts(InventoryDocument document)
{
foreach (var application in document.Applications)
{
foreach (var artifact in application.Artifacts)
{
var searchText = artifact.Name + " "
+ string.Join(" ", artifact.Properties.Select(item => item.Value));
if (searchText.IndexOf("SAP", StringComparison.OrdinalIgnoreCase) >= 0
|| searchText.IndexOf("IDOC", StringComparison.OrdinalIgnoreCase) >= 0
|| searchText.IndexOf("Microsoft.LobServices.Sap", StringComparison.OrdinalIgnoreCase) >= 0)
{
yield return artifact;
}
}
}
}
private static void WriteContentTypes(ZipArchive archive)
{
using (var writer = CreateXmlWriter(archive, "[Content_Types].xml"))
{
writer.WriteStartDocument();
writer.WriteStartElement("Types", ContentTypeNamespace);
WriteContentDefault(writer, "rels", "application/vnd.openxmlformats-package.relationships+xml");
WriteContentDefault(writer, "xml", "application/xml");
WriteContentOverride(
writer,
"/word/document.xml",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml");
WriteContentOverride(
writer,
"/word/styles.xml",
"application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml");
WriteContentOverride(
writer,
"/docProps/core.xml",
"application/vnd.openxmlformats-package.core-properties+xml");
WriteContentOverride(
writer,
"/docProps/app.xml",
"application/vnd.openxmlformats-officedocument.extended-properties+xml");
writer.WriteEndElement();
writer.WriteEndDocument();
}
}
private static void WriteContentDefault(XmlWriter writer, string extension, string contentType)
{
writer.WriteStartElement("Default", ContentTypeNamespace);
writer.WriteAttributeString("Extension", extension);
writer.WriteAttributeString("ContentType", contentType);
writer.WriteEndElement();
}
private static void WriteContentOverride(XmlWriter writer, string partName, string contentType)
{
writer.WriteStartElement("Override", ContentTypeNamespace);
writer.WriteAttributeString("PartName", partName);
writer.WriteAttributeString("ContentType", contentType);
writer.WriteEndElement();
}
private static void WritePackageRelationships(ZipArchive archive)
{
using (var writer = CreateXmlWriter(archive, "_rels/.rels"))
{
writer.WriteStartDocument();
writer.WriteStartElement("Relationships", PackageRelationshipNamespace);
WriteRelationship(
writer,
"rId1",
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",
"word/document.xml");
WriteRelationship(
writer,
"rId2",
"http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties",
"docProps/core.xml");
WriteRelationship(
writer,
"rId3",
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties",
"docProps/app.xml");
writer.WriteEndElement();
writer.WriteEndDocument();
}
}
private static void WriteDocumentRelationships(ZipArchive archive)
{
using (var writer = CreateXmlWriter(archive, "word/_rels/document.xml.rels"))
{
writer.WriteStartDocument();
writer.WriteStartElement("Relationships", PackageRelationshipNamespace);
WriteRelationship(
writer,
"rId1",
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles",
"styles.xml");
writer.WriteEndElement();
writer.WriteEndDocument();
}
}
private static void WriteRelationship(
XmlWriter writer,
string id,
string type,
string target)
{
writer.WriteStartElement("Relationship", PackageRelationshipNamespace);
writer.WriteAttributeString("Id", id);
writer.WriteAttributeString("Type", type);
writer.WriteAttributeString("Target", target);
writer.WriteEndElement();
}
private static void WriteStyles(ZipArchive archive)
{
using (var writer = CreateXmlWriter(archive, "word/styles.xml"))
{
writer.WriteStartDocument();
writer.WriteStartElement("w", "styles", WordNamespace);
WriteStyle(writer, "Normal", "Normal", 20, false, "000000", null);
WriteStyle(writer, "Title", "Titel", 38, true, "1F4E78", null);
WriteStyle(writer, "Subtitle", "Untertitel", 24, false, "5B6573", null);
WriteStyle(writer, "Heading1", "Überschrift 1", 30, true, "1F4E78", "1");
WriteStyle(writer, "Heading2", "Überschrift 2", 24, true, "2F75B5", "2");
WriteStyle(writer, "IntenseQuote", "Intensives Zitat", 20, false, "666666", null);
writer.WriteEndElement();
writer.WriteEndDocument();
}
}
private static void WriteStyle(
XmlWriter writer,
string id,
string name,
int size,
bool bold,
string color,
string outlineLevel)
{
writer.WriteStartElement("w", "style", WordNamespace);
writer.WriteAttributeString("w", "type", WordNamespace, "paragraph");
writer.WriteAttributeString("w", "styleId", WordNamespace, id);
writer.WriteStartElement("w", "name", WordNamespace);
writer.WriteAttributeString("w", "val", WordNamespace, name);
writer.WriteEndElement();
if (!string.IsNullOrWhiteSpace(outlineLevel))
{
writer.WriteStartElement("w", "pPr", WordNamespace);
writer.WriteStartElement("w", "outlineLvl", WordNamespace);
writer.WriteAttributeString("w", "val", WordNamespace, outlineLevel);
writer.WriteEndElement();
writer.WriteEndElement();
}
writer.WriteStartElement("w", "rPr", WordNamespace);
if (bold)
{
writer.WriteElementString("w", "b", WordNamespace, string.Empty);
}
writer.WriteStartElement("w", "color", WordNamespace);
writer.WriteAttributeString("w", "val", WordNamespace, color);
writer.WriteEndElement();
writer.WriteStartElement("w", "sz", WordNamespace);
writer.WriteAttributeString("w", "val", WordNamespace, size.ToString(CultureInfo.InvariantCulture));
writer.WriteEndElement();
writer.WriteEndElement();
writer.WriteEndElement();
}
private static void WriteCoreProperties(ZipArchive archive, InventoryDocument document)
{
using (var writer = CreateXmlWriter(archive, "docProps/core.xml"))
{
writer.WriteStartDocument();
writer.WriteStartElement(
"cp",
"coreProperties",
"http://schemas.openxmlformats.org/package/2006/metadata/core-properties");
writer.WriteAttributeString(
"xmlns",
"dc",
null,
"http://purl.org/dc/elements/1.1/");
writer.WriteAttributeString(
"xmlns",
"dcterms",
null,
"http://purl.org/dc/terms/");
writer.WriteAttributeString(
"xmlns",
"xsi",
null,
"http://www.w3.org/2001/XMLSchema-instance");
writer.WriteElementString(
"dc",
"title",
"http://purl.org/dc/elements/1.1/",
"BizTalk SAP Environment Inventory " + document.EnvironmentName);
writer.WriteElementString(
"dc",
"creator",
"http://purl.org/dc/elements/1.1/",
"BizTalkSapEnvironmentInventory");
writer.WriteElementString(
"dc",
"subject",
"http://purl.org/dc/elements/1.1/",
"BizTalk 2020 SAP Adapter und Anwendungsliste");
writer.WriteStartElement(
"dcterms",
"created",
"http://purl.org/dc/terms/");
writer.WriteAttributeString(
"xsi",
"type",
"http://www.w3.org/2001/XMLSchema-instance",
"dcterms:W3CDTF");
writer.WriteString(document.CompletedUtc.ToString("s", CultureInfo.InvariantCulture) + "Z");
writer.WriteEndElement();
writer.WriteEndElement();
writer.WriteEndDocument();
}
}
private static void WriteApplicationProperties(ZipArchive archive)
{
using (var writer = CreateXmlWriter(archive, "docProps/app.xml"))
{
writer.WriteStartDocument();
writer.WriteStartElement("Properties", ExtendedPropertiesNamespace);
writer.WriteElementString(
"Application",
ExtendedPropertiesNamespace,
"BizTalkSapEnvironmentInventory");
writer.WriteElementString("AppVersion", ExtendedPropertiesNamespace, "1.0");
writer.WriteEndElement();
writer.WriteEndDocument();
}
}
private static XmlWriter CreateXmlWriter(ZipArchive archive, string path)
{
var entry = archive.CreateEntry(path, CompressionLevel.Optimal);
return XmlWriter.Create(
entry.Open(),
new XmlWriterSettings
{
Encoding = new UTF8Encoding(false),
Indent = false,
CloseOutput = true,
CheckCharacters = true
});
}
private static void WriteTitle(XmlWriter writer, string text)
{
WriteParagraph(writer, text, "Title", true, "1F4E78");
}
private static void WriteSubtitle(XmlWriter writer, string text)
{
WriteParagraph(writer, text, "Subtitle", false, "5B6573");
}
private static void WriteHeading(XmlWriter writer, string text, int level)
{
WriteParagraph(
writer,
text,
level <= 1 ? "Heading1" : "Heading2",
true,
level <= 1 ? "1F4E78" : "2F75B5");
}
private static void WriteBullet(XmlWriter writer, string text)
{
WriteParagraph(writer, "• " + text, "Normal", false, null);
}
private static void WriteParagraph(
XmlWriter writer,
string text,
string style,
bool bold,
string color)
{
writer.WriteStartElement("w", "p", WordNamespace);
writer.WriteStartElement("w", "pPr", WordNamespace);
writer.WriteStartElement("w", "pStyle", WordNamespace);
writer.WriteAttributeString("w", "val", WordNamespace, style);
writer.WriteEndElement();
writer.WriteStartElement("w", "spacing", WordNamespace);
writer.WriteAttributeString("w", "after", WordNamespace, "100");
writer.WriteEndElement();
writer.WriteEndElement();
writer.WriteStartElement("w", "r", WordNamespace);
if (bold || !string.IsNullOrWhiteSpace(color))
{
writer.WriteStartElement("w", "rPr", WordNamespace);
if (bold)
{
writer.WriteElementString("w", "b", WordNamespace, string.Empty);
}
if (!string.IsNullOrWhiteSpace(color))
{
writer.WriteStartElement("w", "color", WordNamespace);
writer.WriteAttributeString("w", "val", WordNamespace, color);
writer.WriteEndElement();
}
writer.WriteEndElement();
}
writer.WriteStartElement("w", "t", WordNamespace);
writer.WriteAttributeString("xml", "space", null, "preserve");
writer.WriteString(Safe(text));
writer.WriteEndElement();
writer.WriteEndElement();
writer.WriteEndElement();
}
private static void WriteTable(
XmlWriter writer,
string[] headers,
IEnumerable<string[]> rows)
{
var safeRows = (rows ?? Enumerable.Empty<string[]>()).ToList();
writer.WriteStartElement("w", "tbl", WordNamespace);
WriteTableProperties(writer);
WriteTableRow(writer, headers, true);
foreach (var row in safeRows)
{
var normalized = new string[headers.Length];
for (var index = 0; index < normalized.Length; index++)
{
normalized[index] = row != null && index < row.Length ? row[index] : string.Empty;
}
WriteTableRow(writer, normalized, false);
}
writer.WriteEndElement();
WriteParagraph(writer, string.Empty, "Normal", false, null);
}
private static void WriteTableProperties(XmlWriter writer)
{
writer.WriteStartElement("w", "tblPr", WordNamespace);
writer.WriteStartElement("w", "tblW", WordNamespace);
writer.WriteAttributeString("w", "w", WordNamespace, "0");
writer.WriteAttributeString("w", "type", WordNamespace, "auto");
writer.WriteEndElement();
writer.WriteStartElement("w", "tblBorders", WordNamespace);
foreach (var side in new[] { "top", "left", "bottom", "right", "insideH", "insideV" })
{
writer.WriteStartElement("w", side, WordNamespace);
writer.WriteAttributeString("w", "val", WordNamespace, "single");
writer.WriteAttributeString("w", "sz", WordNamespace, "4");
writer.WriteAttributeString("w", "color", WordNamespace, "B4C6E7");
writer.WriteEndElement();
}
writer.WriteEndElement();
writer.WriteEndElement();
}
private static void WriteTableRow(XmlWriter writer, string[] cells, bool header)
{
writer.WriteStartElement("w", "tr", WordNamespace);
if (header)
{
writer.WriteStartElement("w", "trPr", WordNamespace);
writer.WriteElementString("w", "tblHeader", WordNamespace, string.Empty);
writer.WriteEndElement();
}
foreach (var cell in cells)
{
writer.WriteStartElement("w", "tc", WordNamespace);
writer.WriteStartElement("w", "tcPr", WordNamespace);
if (header)
{
writer.WriteStartElement("w", "shd", WordNamespace);
writer.WriteAttributeString("w", "fill", WordNamespace, "D9EAF7");
writer.WriteEndElement();
}
writer.WriteEndElement();
WriteParagraph(
writer,
Truncate(cell, 6000),
"Normal",
header,
header ? "1F1F1F" : null);
writer.WriteEndElement();
}
writer.WriteEndElement();
}
private static void WriteSectionProperties(XmlWriter writer)
{
writer.WriteStartElement("w", "sectPr", WordNamespace);
writer.WriteStartElement("w", "pgSz", WordNamespace);
writer.WriteAttributeString("w", "w", WordNamespace, "16838");
writer.WriteAttributeString("w", "h", WordNamespace, "11906");
writer.WriteAttributeString("w", "orient", WordNamespace, "landscape");
writer.WriteEndElement();
writer.WriteStartElement("w", "pgMar", WordNamespace);
writer.WriteAttributeString("w", "top", WordNamespace, "900");
writer.WriteAttributeString("w", "right", WordNamespace, "900");
writer.WriteAttributeString("w", "bottom", WordNamespace, "900");
writer.WriteAttributeString("w", "left", WordNamespace, "900");
writer.WriteEndElement();
writer.WriteEndElement();
}
private static string[] Row(string name, string value)
{
return new[] { name, value };
}
private static string Count(ApplicationRecord application, string type)
{
return application.Count(type).ToString(CultureInfo.InvariantCulture);
}
private static int PropertyOrder(string name)
{
var preferred = new[]
{
"DestinationName", "ListenerDestination", "R3SystemName",
"ApplicationServerHost", "MessageServerHost", "SystemNumber",
"GatewayHost", "GatewayService", "ListenerGatewayHost",
"ListenerGatewayService", "ListenerProgramId", "Client",
"Language", "UseSnc", "SncLibrary", "SncPartnerName",
"SncMyName", "SncQop", "UserName", "AffiliateApplicationName"
};
for (var index = 0; index < preferred.Length; index++)
{
if (string.Equals(name, preferred[index], StringComparison.OrdinalIgnoreCase))
{
return index;
}
}
return preferred.Length + 1;
}
private static int SeverityOrder(string severity)
{
if (string.Equals(severity, "Fehler", StringComparison.OrdinalIgnoreCase))
{
return 0;
}
if (string.Equals(severity, "Warnung", StringComparison.OrdinalIgnoreCase))
{
return 1;
}
return 2;
}
private static string Empty(string value)
{
return string.IsNullOrWhiteSpace(value) ? "Nicht ermittelt" : value;
}
private static string Truncate(string value, int maximum)
{
var safe = Safe(value);
return safe.Length <= maximum
? safe
: safe.Substring(0, maximum) + " … [gekürzt]";
}
private static string Safe(string value)
{
return value ?? string.Empty;
}
}
}
@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{9F9E3933-BB12-4F05-984D-8B86C2AB963D}</ProjectGuid>
<OutputType>Exe</OutputType>
<RootNamespace>BizTalkSapEnvironmentInventory.Tests</RootNamespace>
<AssemblyName>BizTalkSapEnvironmentInventory.Tests</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<TargetFrameworkProfile />
<FileAlignment>512</FileAlignment>
<LangVersion>7.3</LangVersion>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE;NETFRAMEWORK</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<PlatformTarget>AnyCPU</PlatformTarget>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE;NETFRAMEWORK</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<PlatformTarget>AnyCPU</PlatformTarget>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.IO.Compression" />
<Reference Include="System.IO.Compression.FileSystem" />
<Reference Include="System.Xml" />
<Reference Include="System.Xml.Linq" />
</ItemGroup>
<ItemGroup>
<Compile Include="Program.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\BizTalkSapEnvironmentInventory\BizTalkSapEnvironmentInventory.csproj">
<Project>{43962556-3DD2-4846-96E2-76B4749F17F2}</Project>
<Name>BizTalkSapEnvironmentInventory</Name>
<Private>True</Private>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
@@ -0,0 +1,31 @@
using System;
using BizTalkSapEnvironmentInventory.Infrastructure;
namespace BizTalkSapEnvironmentInventory.Tests
{
internal static class Program
{
private static int Main(string[] args)
{
try
{
if (args.Length == 2
&& string.Equals(args[0], "--write-sample", StringComparison.OrdinalIgnoreCase))
{
SelfTestRunner.WriteSample(args[1]);
Console.WriteLine("Musterbericht geschrieben: " + args[1]);
return 0;
}
SelfTestRunner.Run();
Console.WriteLine("Alle Tests erfolgreich.");
return 0;
}
catch (Exception exception)
{
Console.Error.WriteLine("Tests fehlgeschlagen: " + exception);
return 1;
}
}
}
}