Initial commit: BizTalk SAP environment inventory
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
artifacts/
|
||||||
|
.vs/
|
||||||
|
*.user
|
||||||
|
*.suo
|
||||||
|
*.log
|
||||||
|
*.docx
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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).
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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%
|
||||||
|
|
||||||
@@ -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&GwServ=sapgw00</Address>
|
||||||
|
<TransportType Name=""WCF-SAP"" />
|
||||||
|
<SendHandler Name=""Snd_SAP_ERP_C1_64_B"" />
|
||||||
|
<TransportTypeData><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>
|
||||||
|
<Password vt=""8"">supersecret</Password>
|
||||||
|
<Action vt=""8"">http://Microsoft.LobServices.Sap/2007/03/Idoc/3/ORDERS05</Action>
|
||||||
|
</CustomProps></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&ListenerGwServ=sapgw00&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""><binding name=""sapBinding"" useSnc=""true"" sncLibrary=""C:\SAP\sapcrypto.dll"" sncPartnerName=""p:SAP/ERP"" /></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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+57
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user