Initial commit: BizTalk application catalog

This commit is contained in:
2026-07-27 14:46:02 +02:00
commit 2b5b97c869
25 changed files with 3026 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
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
+26
View File
@@ -0,0 +1,26 @@
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: BizTalkApplicationCatalog-deploy
path: artifacts\BizTalkApplicationCatalog-deploy
+10
View File
@@ -0,0 +1,10 @@
bin/
obj/
artifacts/
.vs/
*.user
*.suo
*.log
*.xlsx
*.zip
*.zip.txt
+24
View File
@@ -0,0 +1,24 @@
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}") = "BizTalkApplicationCatalog", "src\BizTalkApplicationCatalog\BizTalkApplicationCatalog.csproj", "{41CB5701-3FBC-49F4-856A-6AE930B8513D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkApplicationCatalog.Tests", "tests\BizTalkApplicationCatalog.Tests\BizTalkApplicationCatalog.Tests.csproj", "{6B7CE874-8054-46F5-927C-E9B26927F187}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{41CB5701-3FBC-49F4-856A-6AE930B8513D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{41CB5701-3FBC-49F4-856A-6AE930B8513D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{41CB5701-3FBC-49F4-856A-6AE930B8513D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{41CB5701-3FBC-49F4-856A-6AE930B8513D}.Release|Any CPU.Build.0 = Release|Any CPU
{6B7CE874-8054-46F5-927C-E9B26927F187}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6B7CE874-8054-46F5-927C-E9B26927F187}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6B7CE874-8054-46F5-927C-E9B26927F187}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6B7CE874-8054-46F5-927C-E9B26927F187}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
+220
View File
@@ -0,0 +1,220 @@
# Technische Dokumentation: BEW BizTalk Application Catalog
## 1. Zielbild
Das Werkzeug erstellt je Umgebung eine eigenständige Microsoft-Excel-Arbeitsmappe mit der vollständigen Liste der auf dem lokalen BizTalk Server 2020 installierten Anwendungen und den wichtigsten zugeordneten Kernparametern.
```text
ACC
+-- 1 BizTalk Server 2020 / Windows Server 2019 <-- Tool lokal ausführen
+-- 1 SQL Server für die BizTalk-Datenbanken
PROD
+-- 1 BizTalk Server 2020 / Windows Server 2019 <-- Tool lokal ausführen
+-- 1 SQL Server für die BizTalk-Datenbanken
```
ACC und PROD werden getrennt inventarisiert. Da es pro Umgebung nur einen BizTalk-Knoten gibt, ist kein Node-Abgleich erforderlich.
## 2. Architektur
```text
Administrative cmd.exe
|
+-- run-inventory.cmd ACC|PROD
|
+-- BizTalkApplicationCatalog.exe
|
+-- SystemCollector
| +-- Win32_OperatingSystem
| +-- lokale BizTalk Registry
| +-- MSBTS_GroupSetting
|
+-- BizTalkWmiCollector
| +-- vollständige Anwendungsliste
| +-- Artefaktdetails
| +-- Hosts und Handler
| +-- Abdeckungsstatus pro Klasse
|
+-- XlsxReportWriter
+-- Office Open XML / ZIP
+-- Inline Strings, keine Shared-String-Abhängigkeit
+-- Filter, Freeze Panes, Styles
+-- atomarer Dateiaustausch
```
Die Anwendung referenziert keine `Microsoft.BizTalk.*`-Assembly. Die einzige BizTalk-Schnittstelle ist der lokal installierte WMI-Provider. Dadurch besteht das Deployment nur aus EXE, Konfiguration, Startskript und Dokumentation.
## 3. Toolchain und Kompatibilität
Die Build-Baseline ist absichtlich auf die BizTalk-2020-Umgebung ausgerichtet:
- Visual Studio 2019 / Solution Version 16
- MSBuild 16.x, insbesondere 16.11
- klassisches MSBuild-Projektformat mit `ToolsVersion="15.0"`
- .NET Framework 4.7.2
- C# 7.3
- keine SDK-Style-Projekte
- kein `PackageReference`
- keine `global.json`
- keine NuGet-Abhängigkeit
Damit wird nicht das installierte .NET SDK 8 oder neuer ausgewählt. Der bekannte Fehler „.NET SDK 8.x requires at least MSBuild 17.8.3“ kann bei dieser Solution nicht durch eine SDK-Auswahl entstehen.
## 4. Datenquellen
### 4.1 Pflichtquelle
`MSBTS_Application` liefert die Primärliste aller Anwendungen. Liefert diese Klasse keine Datensätze oder kann sie nicht gelesen werden, gilt der Pflichtabschnitt als fehlgeschlagen und der Prozess endet nach der dennoch versuchten XLSX-Erzeugung mit Exitcode `1`.
Aufgenommene Parameter:
- `Name` beziehungsweise `ApplicationName`
- `Description`
- `Status`
- `IsDefault`, soweit vom Provider geliefert
### 4.2 Optionale Artefaktklassen
| WMI-Klasse | Berichtstyp |
| --- | --- |
| `MSBTS_Orchestration` | Orchestrierung |
| `MSBTS_SendPort` | Send Port |
| `MSBTS_SendPortGroup` | Send Port Group |
| `MSBTS_ReceivePort` | Receive Port |
| `MSBTS_ReceiveLocation` | Receive Location |
| `MSBTS_Assembly` | Assembly |
| `MSBTS_Schema` | Schema |
| `MSBTS_Map` | Map |
| `MSBTS_Pipeline` | Pipeline |
Die WMI-Klassen können abhängig von Installation, Providerstand und Berechtigung unterschiedliche Properties anbieten. Deshalb fragt der Collector `SELECT *` ab und liest bekannte Properties defensiv. Eine nicht vorhandene Property wird leer gelassen und beendet die Klasse nicht.
Receive Ports werden vor Receive Locations gelesen. Fehlt an einer Receive Location die direkte `ApplicationName`, wird die Anwendung über `ReceivePortName` bestmöglich aufgelöst.
### 4.3 Hosts und Handler
- `MSBTS_HostSetting`
- `MSBTS_HostInstance`
- `MSBTS_ReceiveHandler`
- `MSBTS_SendHandler2`
- Fallback `MSBTS_SendHandler`
Erfasst werden Name, Server, Status, Hosttyp, Windows-Gruppe, 32-Bit-Kennzeichen, Trusted-Kennzeichen und Adaptername, soweit vorhanden.
### 4.4 Systeminformationen
- `Win32_OperatingSystem`
- `HKLM\SOFTWARE\Microsoft\BizTalk Server\3.0`
- `HKLM\SOFTWARE\Microsoft\BizTalk Server\3.0\Administration`
- `MSBTS_GroupSetting`
Registry-Werte werden in 64- und 32-Bit-Ansicht gelesen. Kommandozeilenwerte für Management Server und Datenbank überschreiben ermittelte Werte.
## 5. Abdeckungsmodell
Jede abgefragte BizTalk-WMI-Klasse erzeugt einen Datensatz im Blatt `Abdeckung`:
| Status | Bedeutung |
| --- | --- |
| `Vollständig` | Abfrage erfolgreich; die angegebene Zeilenzahl ist technisch belegt |
| `Begrenzt` | Abfrage erfolgreich, aber das konfigurierte Zeilenlimit wurde erreicht |
| `Nicht verfügbar` | Klasse oder Berechtigung nicht verfügbar; eine Null ist nicht fachlich bestätigt |
| `Fehler` | Pflichtquelle lieferte kein verwertbares Ergebnis |
Dieses Modell verhindert die irreführende Interpretation eines fehlenden WMI-Ergebnisses als „keine Artefakte vorhanden“.
`MaxRowsPerArtifactType` begrenzt optional die Details pro Artefakttyp. Der Standard ist `10000`. Die Anwendungsliste aus `MSBTS_Application` wird nicht begrenzt.
## 6. Excel-Erzeugung
Die `.xlsx`-Datei ist ein OPC-/ZIP-Paket mit folgenden Kernteilen:
```text
[Content_Types].xml
_rels/.rels
docProps/core.xml
docProps/app.xml
xl/workbook.xml
xl/_rels/workbook.xml.rels
xl/styles.xml
xl/worksheets/sheet1.xml ... sheet10.xml
```
Zelltexte werden als `inlineStr` geschrieben. Dadurch wird keine Shared-String-Tabelle im Arbeitsspeicher aufgebaut. Zahlen werden als numerische Zellen gespeichert und können in Excel direkt summiert oder gefiltert werden.
Der Writer:
1. erzeugt eine eindeutige temporäre Datei im Zielordner,
2. schreibt und schließt alle XML-/ZIP-Teile,
3. entfernt eine gegebenenfalls vorhandene Zieldatei gleichen Namens,
4. verschiebt die vollständige temporäre Datei atomar auf den Zielnamen,
5. löscht temporäre Reste auch im Fehlerfall.
Ein einzelner Zelltext wird auf das Excel-Limit von 32.767 Zeichen begrenzt. Nicht XML-konforme Steuerzeichen werden entfernt.
## 7. Resilienz
- `SafeCollector` isoliert voneinander unabhängige Abschnitte.
- Jeder Abschnitt schreibt Start, Ende, Dauer und Fehler in Konsole und Log.
- Optionale WMI-Klassen werden einzeln behandelt.
- Timeout und maximale Detailzeilen sind in `App.config` konfigurierbar.
- Die XLSX wird auch bei optionalen Lücken erstellt.
- Die vollständige Anwendungsliste ist ein Pflichtabschnitt.
- Findings erklären Lücken und nennen eine konkrete Prüfung.
## 8. Datenschutz und Secrets
Das Inventar soll keine Credentials enthalten. Bekannte Kennwort-, Secret- und Tokenmuster in Text- und Adresswerten werden durch `[REDACTED]` ersetzt. Das ersetzt keine Schutzklassifizierung: interne Adressen, Servernamen, Hostgruppen und Anwendungsbezeichnungen bleiben sensible Betriebsinformationen.
Es werden keine Kennwörter abgefragt, keine Bindings exportiert und keine aktiven Endpunktverbindungen getestet.
## 9. Build, Test und Paketierung
Windows-Build:
```cmd
scripts\build-release.cmd
```
Der Build:
1. findet MSBuild über `vswhere.exe` oder `PATH`,
2. baut `Release|Any CPU`,
3. führt das Testprogramm aus,
4. führt den Self-Test der eigentlichen EXE aus.
Der Self-Test benötigt kein BizTalk. Er prüft:
- Secret-Redaktion
- Erstellung aller zehn Tabellenblätter
- Vorhandensein der erforderlichen OPC-/XLSX-Teile
- XML-Wohlgeformtheit aller Paketbestandteile
- erwartete Blattnamen
- Abwesenheit des Testkennworts
Deployment:
```cmd
scripts\package-release.cmd
```
Quell-ZIP und certutil-dekodierbarer Text:
```cmd
scripts\package-source.cmd
```
## 10. Validierung in ACC und PROD
Je Umgebung:
1. EXE mit `--self-test` starten.
2. Inventar in einer administrativen `cmd.exe` ausführen.
3. Exitcode und Log prüfen.
4. XLSX öffnen und Blatt `Abdeckung` prüfen.
5. Zahl der Anwendungen mit der BizTalk Administration Console vergleichen.
6. Stichproben für Ports, Orchestrierungen und Assemblies durchführen.
7. XLSX und Log im geschützten Umgebungsordner archivieren.
ACC- und PROD-Dateien dürfen nicht zusammengeführt werden, ohne die Spalten `Umgebung` und `Server` beizubehalten.
+210
View File
@@ -0,0 +1,210 @@
# BEW BizTalk Application Catalog
`BEW BizTalk Application Catalog` inventarisiert die auf einem BizTalk Server 2020 installierten Anwendungen und erzeugt eine kompakte, filterbare Microsoft-Excel-Arbeitsmappe (`.xlsx`).
Das Tool wird lokal 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 auf Windows Server 2019
- einem separaten SQL Server für die BizTalk-Datenbanken
Microsoft Excel, Microsoft Office, PowerShell, Internetzugriff und ein .NET SDK werden auf dem Zielserver nicht benötigt.
## Ergebnis
Die erzeugte Arbeitsmappe enthält zehn Tabellenblätter:
| Blatt | Inhalt |
| --- | --- |
| `Übersicht` | Umgebung, Server, BizTalk-/Windows-Metadaten, Gesamtzahlen und Laufstatus |
| `Anwendungen` | vollständige, kompakte Anwendungsliste mit Status, Beschreibung und Artefaktzahlen |
| `Artefakte` | konsolidierte technische Detailansicht |
| `Ports` | Send Ports, Send Port Groups, Receive Ports und Receive Locations |
| `Orchestrierungen` | Orchestrierungen, Status und Hostzuordnung |
| `Schemas-Maps-Pipelines` | Schemas, Maps und Pipelines |
| `Assemblies` | bereitgestellte BizTalk-Assemblies |
| `Hosts-Handler` | Hosts, Hostinstanzen sowie Receive-/Send-Handler |
| `Abdeckung` | Erfolg, Zeilenzahl und mögliche Lücken jeder WMI-Datenquelle |
| `Findings` | Warnungen, Fehler und empfohlene Prüfungen |
Alle Detailblätter besitzen Filter, fixierte Kopfzeilen und angepasste Spaltenbreiten.
## Kernparameter je Anwendung
Das Blatt `Anwendungen` enthält unter anderem:
- Umgebung und ausführenden BizTalk Server
- Anwendungsname, Beschreibung, Status und Standardanwendungskennzeichen
- Gesamtzahl zugeordneter Artefakte
- Anzahl Orchestrierungen
- Anzahl Send Ports und Send Port Groups
- Anzahl Receive Ports und Receive Locations
- Anzahl Assemblies, Schemas, Maps und Pipelines
- erkannte Hosts beziehungsweise Handler
- verwendete Adapter
- expliziten Status der Detailabdeckung
Die vollständige Anwendungsliste stammt aus `MSBTS_Application`. Eine nicht verfügbare optionale Artefaktklasse wird nicht stillschweigend als fachliche Null interpretiert, sondern im Blatt `Abdeckung` ausgewiesen.
## Sicherheits- und Betriebsprinzip
- ausschließlich lokale, lesende WMI- und Registry-Abfragen
- keine Änderungen an Anwendungen, Ports, Hosts oder Runtime-Instanzen
- keine direkte SQL-Abfrage und keine Datenbankänderung
- keine Office-Automation und keine COM-Interop
- zusätzliche Redigierung versehentlich gelieferter Kennwort-/Tokenwerte
- atomare XLSX-Erzeugung über eine temporäre Datei
- Fehler optionaler WMI-Klassen stoppen die übrige Erfassung nicht
- identische Fortschrittsausgabe auf Konsole und in der Logdatei
Die XLSX- und Logdatei enthält interne Server-, Anwendungs-, Host- und Endpunktinformationen und muss entsprechend geschützt abgelegt werden.
## Voraussetzungen auf ACC und PROD
- lokale Ausführung auf dem jeweiligen BizTalk Server 2020
- Windows Server 2019
- administrative `cmd.exe` empfohlen
- Konto mit Leserechten auf `root\MicrosoftBizTalkServer`, regulär ein BizTalk-Administratoren- oder geeignetes Operatorenkonto
- .NET Framework 4.7.2 oder höher
- funktionierender lokaler BizTalk-WMI-Provider
Microsoft Office ist ausdrücklich keine Voraussetzung.
## Schnellstart
1. Deployment-Ordner auf den BizTalk Server kopieren.
2. Administrative `cmd.exe` öffnen.
3. Self-Test ohne BizTalk-Zugriff ausführen.
4. Inventar für die jeweilige Umgebung erzeugen.
Self-Test:
```cmd
BizTalkApplicationCatalog.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
BizTalkApplicationCatalog.exe ^
--environment ACC ^
--output C:\BizTalk-Doku\ACC
```
## Konsolenausgabe
Das Tool zeigt laufend, was es gerade erfasst und wann die Excel-Datei geschrieben wurde:
```text
[INFO] Starte Abschnitt: Anwendungen und Artefakte
[INFO] 34 installierte BizTalk-Anwendung(en) gefunden.
[INFO] Send Port: 87 Datensatz/Datensätze.
[INFO] Erzeuge Microsoft-Excel-Datei: C:\BizTalk-Doku\ACC\...
[INFO] Excel-Datei erfolgreich erzeugt: C:\BizTalk-Doku\ACC\...
```
## Ausgabedateien
```text
BizTalk-Anwendungsinventar-ACC-BIZTALKSERVER-20260727-150000.xlsx
BizTalk-Anwendungsinventar-ACC-BIZTALKSERVER-20260727-150000.log
```
## Optionen und Exitcodes
| Option | Bedeutung |
| --- | --- |
| `--environment NAME` | Umgebung, regulär `ACC` oder `PROD` |
| `--output PFAD` | Zielordner für XLSX und Log |
| `--management-server NAME` | optionaler Management-SQL-Server-Hinweis |
| `--management-database NAME` | optionale Management-Datenbank; Fallback `BizTalkMgmtDb` |
| `--self-test` | prüft XLSX-Struktur, Tabellen und Secret-Redaktion |
| `--help` | zeigt die Hilfe |
| Exitcode | Bedeutung |
| ---: | --- |
| `0` | vollständige Anwendungserfassung und XLSX erfolgreich |
| `1` | XLSX erzeugt, aber ein Pflichtabschnitt war unvollständig |
| `2` | Aufruf-, Ausgabe- oder Berichtserzeugungsfehler |
## Build mit Visual Studio 2019
Die Solution vermeidet bewusst den beim früheren IIS Inventory aufgetretenen .NET-SDK-/MSBuild-Konflikt:
| Komponente | Vorgabe |
| --- | --- |
| Ziel | .NET Framework 4.7.2 |
| Sprache | C# 7.3 |
| Solution | Visual Studio 2019, Formatversion 16 |
| Projektformat | klassisches MSBuild mit `ToolsVersion="15.0"` |
| MSBuild | 16.x, einschließlich 16.11 |
| NuGet | keine Pakete und kein Restore |
| .NET SDK / `global.json` | nicht erforderlich |
Voraussetzung auf dem Buildrechner ist das `.NET Framework 4.7.2 Developer/Targeting Pack`.
```cmd
scripts\build-release.cmd
scripts\package-release.cmd
```
Das Deployment liegt anschließend unter:
```text
artifacts\BizTalkApplicationCatalog-deploy\
```
## Quellpaket und Base64-Text
Für eine dateibasierte Übergabe:
```cmd
scripts\package-source.cmd
```
Erzeugt werden:
```text
artifacts\BizTalkApplicationCatalog-source.zip
artifacts\BizTalkApplicationCatalog-source.zip.txt
```
Dekodieren unter Windows:
```cmd
certutil -decode BizTalkApplicationCatalog-source.zip.txt BizTalkApplicationCatalog-source.zip
```
## Gitea
Das Repository enthält `.gitea/workflows/build.yml`. Ein Windows-Runner mit Visual Studio 2019 Build Tools und .NET Framework 4.7.2 Developer Pack baut die Solution, führt Tests und Self-Test aus und stellt den Deployment-Ordner als Artefakt bereit.
## Troubleshooting
### Solution lässt sich in VS 2019 nicht öffnen
Prüfen, dass wirklich diese klassische Solution geöffnet wird und das .NET Framework 4.7.2 Developer Pack installiert ist. Das Repository enthält weder SDK-Style-Projekte noch `global.json` oder `PackageReference`.
### Keine Anwendungen
- Tool lokal auf dem BizTalk Server ausführen
- Konto und WMI-Leserechte prüfen
- Namespace `root\MicrosoftBizTalkServer` prüfen
- BizTalk Administration Console auf demselben Konto testen
### Einzelne Zählwerte sind 0
Zuerst das Blatt `Abdeckung` prüfen. Nur bei Status `Vollständig` ist die Null durch eine erfolgreich gelesene WMI-Klasse belegt. Bei `Nicht verfügbar` oder `Begrenzt` muss die Ursache geprüft werden.
Weitere technische Details stehen in [Dokumentation.md](Dokumentation.md).
+51
View File
@@ -0,0 +1,51 @@
@echo off
setlocal EnableExtensions
set "APPDIR=%~dp0"
set "EXE=%APPDIR%BizTalkApplicationCatalog.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-Anwendungsinventar\%ENVIRONMENT%"
) else (
set "OUTPUT=%~2"
)
echo ============================================================
echo BEW BizTalk Application Catalog
echo Umgebung: %ENVIRONMENT%
echo Server: %COMPUTERNAME%
echo Ausgabe: %OUTPUT%
echo Modus: ausschliesslich lesend
echo ============================================================
echo.
"%EXE%" --environment "%ENVIRONMENT%" --output "%OUTPUT%"
set "EXITCODE=%ERRORLEVEL%"
echo.
if "%EXITCODE%"=="0" (
echo Inventar erfolgreich erstellt.
) else if "%EXITCODE%"=="1" (
echo Inventar mit Teilfehler erstellt. XLSX, Abdeckung, Findings und Log pruefen.
) else (
echo Inventar konnte nicht erstellt 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
+43
View File
@@ -0,0 +1,43 @@
@echo off
setlocal EnableExtensions
set "ROOT=%~dp0.."
set "SOLUTION=%ROOT%\BizTalkApplicationCatalog.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 .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 und ohne NuGet-Restore ...
"%MSBUILD%" "%SOLUTION%" /m /t:Rebuild /p:Configuration=Release /p:Platform="Any CPU"
if errorlevel 1 exit /b %ERRORLEVEL%
echo Fuehre automatisierte Tests aus ...
"%ROOT%\tests\BizTalkApplicationCatalog.Tests\bin\Release\BizTalkApplicationCatalog.Tests.exe"
if errorlevel 1 exit /b %ERRORLEVEL%
echo Fuehre Self-Test der Anwendung aus ...
"%ROOT%\src\BizTalkApplicationCatalog\bin\Release\BizTalkApplicationCatalog.exe" --self-test
exit /b %ERRORLEVEL%
+24
View File
@@ -0,0 +1,24 @@
@echo off
setlocal EnableExtensions
set "ROOT=%~dp0.."
set "BIN=%ROOT%\src\BizTalkApplicationCatalog\bin\Release"
set "ARTIFACTS=%ROOT%\artifacts"
set "DEPLOY=%ARTIFACTS%\BizTalkApplicationCatalog-deploy"
call "%ROOT%\scripts\build-release.cmd"
if errorlevel 1 exit /b %ERRORLEVEL%
if exist "%DEPLOY%" rmdir /s /q "%DEPLOY%"
mkdir "%DEPLOY%"
copy "%BIN%\BizTalkApplicationCatalog.exe" "%DEPLOY%\" >nul
copy "%BIN%\BizTalkApplicationCatalog.exe.config" "%DEPLOY%\" >nul
if exist "%BIN%\BizTalkApplicationCatalog.pdb" copy "%BIN%\BizTalkApplicationCatalog.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
+31
View File
@@ -0,0 +1,31 @@
@echo off
setlocal EnableExtensions
set "ROOT=%~dp0.."
set "ARTIFACTS=%ROOT%\artifacts"
set "ZIP=%ARTIFACTS%\BizTalkApplicationCatalog-source.zip"
set "TEXT=%ZIP%.txt"
if not exist "%ARTIFACTS%" mkdir "%ARTIFACTS%"
if exist "%ZIP%" del /q "%ZIP%"
if exist "%TEXT%" del /q "%TEXT%"
where tar >nul 2>nul
if errorlevel 1 (
echo FEHLER: tar.exe wird fuer das Quellarchiv benoetigt.
exit /b 2
)
pushd "%ROOT%"
tar.exe -a -c -f "%ZIP%" --exclude=.git --exclude=bin --exclude=obj --exclude=artifacts .
set "TAR_EXIT=%ERRORLEVEL%"
popd
if not "%TAR_EXIT%"=="0" exit /b %TAR_EXIT%
certutil -f -encode "%ZIP%" "%TEXT%" >nul
if errorlevel 1 exit /b %ERRORLEVEL%
echo Quellarchiv: %ZIP%
echo Base64-Text: %TEXT%
echo Dekodieren: certutil -decode BizTalkApplicationCatalog-source.zip.txt BizTalkApplicationCatalog-source.zip
exit /b 0
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup useLegacyV2RuntimeActivationPolicy="true">
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
<appSettings>
<add key="WmiTimeoutSeconds" value="30" />
<add key="MaxRowsPerArtifactType" value="10000" />
</appSettings>
</configuration>
@@ -0,0 +1,64 @@
<?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>{41CB5701-3FBC-49F4-856A-6AE930B8513D}</ProjectGuid>
<OutputType>Exe</OutputType>
<RootNamespace>BizTalkApplicationCatalog</RootNamespace>
<AssemblyName>BizTalkApplicationCatalog</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>
<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>
<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.Xml" />
<Reference Include="System.Xml.Linq" />
</ItemGroup>
<ItemGroup>
<Compile Include="Collectors\BizTalkWmiCollector.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\XlsxReportWriter.cs" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
@@ -0,0 +1,421 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Management;
using System.Runtime.InteropServices;
using BizTalkApplicationCatalog.Infrastructure;
using BizTalkApplicationCatalog.Models;
namespace BizTalkApplicationCatalog.Collectors
{
/// <summary>
/// Liest die installierten Anwendungen und zugehörigen Artefakte über den lokalen BizTalk-WMI-Provider.
/// </summary>
internal sealed class BizTalkWmiCollector
{
private readonly InventoryDocument document;
private readonly ConsoleFileLogger logger;
private readonly ManagementScope scope;
private readonly TimeSpan timeout;
private readonly int maxRowsPerType;
private readonly Dictionary<string, string> receivePortApplications =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
public BizTalkWmiCollector(
InventoryDocument document,
ConsoleFileLogger logger,
int timeoutSeconds,
int maxRowsPerType)
{
this.document = document;
this.logger = logger;
timeout = TimeSpan.FromSeconds(Math.Max(5, timeoutSeconds));
this.maxRowsPerType = Math.Max(100, maxRowsPerType);
scope = new ManagementScope(
@"\\" + Environment.MachineName + @"\root\MicrosoftBizTalkServer");
scope.Options.Timeout = timeout;
}
/// <summary>
/// Stellt zuerst die vollständige Primärliste her und ergänzt anschließend optionale Details.
/// </summary>
public void Collect()
{
scope.Connect();
CollectApplications();
// Receive Ports werden vor Receive Locations gelesen, damit deren Anwendung
// auch dann aufgelöst werden kann, wenn die Location sie nicht direkt liefert.
foreach (var descriptor in ArtifactDescriptors())
{
CollectOptionalArtifacts(descriptor);
}
CollectOptionalHosts("MSBTS_HostSetting", "Host");
CollectOptionalHosts("MSBTS_HostInstance", "Hostinstanz");
CollectOptionalHosts("MSBTS_ReceiveHandler", "Receive Handler");
if (!CollectOptionalHosts("MSBTS_SendHandler2", "Send Handler"))
{
CollectOptionalHosts("MSBTS_SendHandler", "Send Handler");
}
document.Applications.Sort((left, right) =>
string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase));
document.Artifacts.Sort(CompareArtifacts);
document.Hosts.Sort((left, right) =>
{
var category = string.Compare(left.Category, right.Category, StringComparison.OrdinalIgnoreCase);
return category != 0
? category
: string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase);
});
}
private void CollectApplications()
{
var count = 0;
foreach (var row in Query("SELECT * FROM MSBTS_Application"))
{
using (row)
{
var name = First(row, "Name", "ApplicationName");
if (string.IsNullOrWhiteSpace(name)) continue;
document.Applications.Add(new ApplicationRecord
{
Name = name,
Description = First(row, "Description"),
Status = FormatStatus(First(row, "Status")),
IsDefault = FormatBoolean(First(row, "IsDefault")),
Source = "MSBTS_Application"
});
count++;
}
}
document.Coverage.Add(new CoverageRecord
{
DataSource = "MSBTS_Application",
Status = count > 0 ? "Vollständig" : "Fehler",
RowCount = count,
Required = "Ja",
Message = count > 0
? "Primärquelle der vollständigen Anwendungsliste."
: "Keine Anwendung geliefert."
});
if (count == 0)
{
throw new InvalidOperationException(
"MSBTS_Application lieferte keine Anwendungen. Zielserver und Berechtigung prüfen.");
}
logger.Info(count + " installierte BizTalk-Anwendung(en) gefunden.");
}
private void CollectOptionalArtifacts(ArtifactDescriptor descriptor)
{
try
{
var count = 0;
var truncated = false;
foreach (var row in Query("SELECT * FROM " + descriptor.ClassName))
{
using (row)
{
if (count >= maxRowsPerType)
{
truncated = true;
break;
}
var record = CreateArtifact(row, descriptor.DisplayName);
if (string.IsNullOrWhiteSpace(record.Name)) continue;
if (record.Type == "Receive Port")
{
receivePortApplications[record.Name] = record.ApplicationName;
}
if (record.Type == "Receive Location"
&& string.IsNullOrWhiteSpace(record.ApplicationName)
&& receivePortApplications.ContainsKey(record.ParentName))
{
record.ApplicationName = receivePortApplications[record.ParentName];
}
document.Artifacts.Add(record);
count++;
}
}
document.Coverage.Add(new CoverageRecord
{
DataSource = descriptor.ClassName,
Status = truncated ? "Begrenzt" : "Vollständig",
RowCount = count,
Required = "Nein",
Message = truncated
? "Detailzeilen auf " + maxRowsPerType + " begrenzt."
: "WMI-Klasse erfolgreich gelesen."
});
logger.Info(descriptor.DisplayName + ": " + count + " Datensatz/Datensätze.");
if (truncated)
{
AddFinding(
"Warnung",
descriptor.DisplayName,
"Detailzeilen wurden bei " + maxRowsPerType + " Einträgen begrenzt.",
"MaxRowsPerArtifactType kontrolliert erhöhen und Inventar erneut ausführen.");
}
}
catch (Exception exception) when (IsRecoverableWmiException(exception))
{
document.Coverage.Add(new CoverageRecord
{
DataSource = descriptor.ClassName,
Status = "Nicht verfügbar",
RowCount = 0,
Required = "Nein",
Message = exception.Message
});
AddFinding(
"Hinweis",
descriptor.DisplayName,
"Optionale WMI-Klasse konnte nicht gelesen werden: " + exception.Message,
"Berechtigung und Verfügbarkeit der WMI-Klasse prüfen; Wert 0 nicht als fachlich bestätigt werten.");
logger.Warning(descriptor.ClassName + " nicht verfügbar: " + exception.Message);
}
}
private bool CollectOptionalHosts(string className, string category)
{
try
{
var count = 0;
foreach (var row in Query("SELECT * FROM " + className))
{
using (row)
{
var name = First(row, "Name", "HostName", "AdapterName", "RunningServer");
if (string.IsNullOrWhiteSpace(name)) continue;
document.Hosts.Add(new HostRecord
{
Category = category,
Name = name,
Server = First(row, "RunningServer", "ServerName"),
Status = FormatStatus(First(row, "ServiceState", "Status")),
Type = First(row, "HostType"),
WindowsGroup = First(row, "NTGroupName"),
Is32BitOnly = FormatBoolean(First(row, "IsHost32BitOnly")),
Trusted = FormatBoolean(First(row, "AuthTrusted")),
AdapterName = First(row, "AdapterName")
});
count++;
}
}
document.Coverage.Add(new CoverageRecord
{
DataSource = className,
Status = "Vollständig",
RowCount = count,
Required = "Nein",
Message = "WMI-Klasse erfolgreich gelesen."
});
logger.Info(category + ": " + count + " Datensatz/Datensätze.");
return true;
}
catch (Exception exception) when (IsRecoverableWmiException(exception))
{
document.Coverage.Add(new CoverageRecord
{
DataSource = className,
Status = "Nicht verfügbar",
RowCount = 0,
Required = "Nein",
Message = exception.Message
});
logger.Warning(className + " nicht verfügbar: " + exception.Message);
return false;
}
}
private ArtifactRecord CreateArtifact(ManagementBaseObject row, string type)
{
var record = new ArtifactRecord
{
Type = type,
ApplicationName = First(row, "ApplicationName", "Application"),
Name = First(
row,
"Name",
"AssemblyName",
"FullName",
"ReceivePortName",
"OrchestrationName"),
Status = FormatStatus(First(row, "Status", "ServiceStatus", "IsDisabled")),
HostName = First(row, "HostName", "SendHandler", "ReceiveHandler"),
AdapterName = First(
row,
"PTTransportType",
"AdapterName",
"TransportType"),
ParentName = First(row, "ReceivePortName", "SendPortGroupName"),
Address = SensitiveDataSanitizer.Sanitize(First(
row,
"PTAddress",
"InboundTransportURL",
"Address"))
};
AddProperty(row, record, "Description");
AddProperty(row, record, "IsTwoWay");
AddProperty(row, record, "IsDynamic");
AddProperty(row, record, "IsDisabled");
AddProperty(row, record, "ReceivePipeline");
AddProperty(row, record, "SendPipeline");
AddProperty(row, record, "STTransportType");
AddProperty(row, record, "STAddress");
AddProperty(row, record, "AssemblyName");
AddProperty(row, record, "FullName");
AddProperty(row, record, "TargetNameSpace");
AddProperty(row, record, "RootName");
AddProperty(row, record, "Tracking");
return record;
}
private static IEnumerable<ArtifactDescriptor> ArtifactDescriptors()
{
return new[]
{
new ArtifactDescriptor("MSBTS_Orchestration", "Orchestrierung"),
new ArtifactDescriptor("MSBTS_SendPort", "Send Port"),
new ArtifactDescriptor("MSBTS_SendPortGroup", "Send Port Group"),
new ArtifactDescriptor("MSBTS_ReceivePort", "Receive Port"),
new ArtifactDescriptor("MSBTS_ReceiveLocation", "Receive Location"),
new ArtifactDescriptor("MSBTS_Assembly", "Assembly"),
new ArtifactDescriptor("MSBTS_Schema", "Schema"),
new ArtifactDescriptor("MSBTS_Map", "Map"),
new ArtifactDescriptor("MSBTS_Pipeline", "Pipeline")
};
}
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 void AddFinding(string severity, string area, string message, string action)
{
document.Findings.Add(new Finding
{
Severity = severity,
Area = area,
Message = message,
RecommendedAction = action
});
}
private static void AddProperty(
ManagementBaseObject row,
ArtifactRecord target,
string propertyName)
{
var value = First(row, propertyName);
if (!string.IsNullOrWhiteSpace(value))
{
target.Properties.Add(new NameValueRecord(
propertyName,
SensitiveDataSanitizer.RedactProperty(propertyName, value),
"WMI"));
}
}
private static string First(ManagementBaseObject row, params string[] names)
{
foreach (var name in names)
{
try
{
var value = row[name];
if (value == null) continue;
var text = ConvertValue(value);
if (!string.IsNullOrWhiteSpace(text)) return text;
}
catch (Exception exception) when (IsRecoverableWmiException(exception))
{
// Die Eigenschaft ist in dieser BizTalk-Version/Klasse nicht vorhanden.
}
}
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 bool IsRecoverableWmiException(Exception exception)
{
return exception is ManagementException
|| exception is COMException
|| exception is UnauthorizedAccessException;
}
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 static string FormatBoolean(string value)
{
if (string.IsNullOrWhiteSpace(value)) return "Unbekannt";
if (value.Equals("True", StringComparison.OrdinalIgnoreCase) || value == "1") return "Ja";
if (value.Equals("False", StringComparison.OrdinalIgnoreCase) || value == "0") return "Nein";
return value;
}
private static int CompareArtifacts(ArtifactRecord left, ArtifactRecord right)
{
var application = string.Compare(
left.ApplicationName, right.ApplicationName, StringComparison.OrdinalIgnoreCase);
if (application != 0) return application;
var type = string.Compare(left.Type, right.Type, StringComparison.OrdinalIgnoreCase);
return type != 0
? type
: string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase);
}
private sealed class ArtifactDescriptor
{
public ArtifactDescriptor(string className, string displayName)
{
ClassName = className;
DisplayName = displayName;
}
public string ClassName { get; private set; }
public string DisplayName { get; private set; }
}
}
}
@@ -0,0 +1,223 @@
using System;
using System.Globalization;
using System.Management;
using System.Reflection;
using Microsoft.Win32;
using BizTalkApplicationCatalog.Configuration;
using BizTalkApplicationCatalog.Models;
namespace BizTalkApplicationCatalog.Collectors
{
/// <summary>
/// Erfasst lokale Windows-, BizTalk- und Gruppenmetadaten ausschließlich lesend.
/// </summary>
internal sealed class SystemCollector
{
private const string ProductKey = @"SOFTWARE\Microsoft\BizTalk Server\3.0";
private const string AdministrationKey = ProductKey + @"\Administration";
private readonly CommandLineOptions options;
public SystemCollector(CommandLineOptions options)
{
this.options = options;
}
public void Collect(InventoryDocument document)
{
document.ComputerName = Environment.MachineName;
document.ToolVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString();
Add(document, "Ausführender Benutzer", Environment.UserDomainName + "\\" + Environment.UserName, "Prozess");
Add(document, "64-Bit-Betriebssystem", Environment.Is64BitOperatingSystem ? "Ja" : "Nein", "Prozess");
Add(document, "64-Bit-Prozess", Environment.Is64BitProcess ? "Ja" : "Nein", "Prozess");
Add(document, ".NET Runtime", Environment.Version.ToString(), "Prozess");
CollectOperatingSystem(document);
CollectRegistry(document);
CollectGroupSetting(document);
if (!string.IsNullOrWhiteSpace(options.ManagementServer))
{
document.ManagementServer = options.ManagementServer;
}
if (!string.IsNullOrWhiteSpace(options.ManagementDatabase))
{
document.ManagementDatabase = options.ManagementDatabase;
}
if (string.IsNullOrWhiteSpace(document.ManagementDatabase))
{
document.ManagementDatabase = "BizTalkMgmtDb";
}
Add(document, "BizTalk Management SQL Server", Unknown(document.ManagementServer), "Ermittelt/Parameter");
Add(document, "BizTalk Management Database", document.ManagementDatabase, "Ermittelt/Parameter");
if (document.EnvironmentName != "ACC" && document.EnvironmentName != "PROD")
{
document.Findings.Add(new Finding
{
Severity = "Warnung",
Area = "Aufruf",
Message = "Die Umgebung ist weder ACC noch PROD: " + document.EnvironmentName,
RecommendedAction = "Umgebung und Zielserver vor der Ablage bestätigen."
});
}
}
private static void CollectOperatingSystem(InventoryDocument document)
{
using (var searcher = new ManagementObjectSearcher(
"root\\cimv2",
"SELECT Caption,Version,BuildNumber,OSArchitecture,LastBootUpTime FROM Win32_OperatingSystem"))
{
foreach (ManagementObject row in searcher.Get())
{
using (row)
{
Add(document, "Betriebssystem", Value(row, "Caption"), "Win32_OperatingSystem");
Add(document, "Windows-Version", Value(row, "Version"), "Win32_OperatingSystem");
Add(document, "Windows-Build", Value(row, "BuildNumber"), "Win32_OperatingSystem");
Add(document, "Architektur", Value(row, "OSArchitecture"), "Win32_OperatingSystem");
Add(document, "Letzter Systemstart", WmiDate(Value(row, "LastBootUpTime")), "Win32_OperatingSystem");
break;
}
}
}
}
private static void CollectRegistry(InventoryDocument document)
{
foreach (var view in new[] { RegistryView.Registry64, RegistryView.Registry32 })
{
using (var baseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, view))
using (var product = baseKey.OpenSubKey(ProductKey, false))
{
if (product != null)
{
AddRegistry(document, product, "ProductName", "BizTalk Produktname", view);
AddRegistry(document, product, "ProductVersion", "BizTalk Produktversion", view);
AddRegistry(document, product, "Edition", "BizTalk Edition", view);
}
}
using (var baseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, view))
using (var administration = baseKey.OpenSubKey(AdministrationKey, false))
{
if (administration == null)
{
continue;
}
if (string.IsNullOrWhiteSpace(document.ManagementServer))
{
document.ManagementServer = FirstRegistry(
administration, "MgmtDBServer", "ManagementDBServer");
}
if (string.IsNullOrWhiteSpace(document.ManagementDatabase))
{
document.ManagementDatabase = FirstRegistry(
administration, "MgmtDBName", "ManagementDBName");
}
}
}
}
private static void CollectGroupSetting(InventoryDocument document)
{
try
{
using (var searcher = new ManagementObjectSearcher(
@"root\MicrosoftBizTalkServer", "SELECT * FROM MSBTS_GroupSetting"))
{
foreach (ManagementObject row in searcher.Get())
{
using (row)
{
var server = Value(row, "MgmtDbServerName");
var database = Value(row, "MgmtDbName");
if (!string.IsNullOrWhiteSpace(server)) document.ManagementServer = server;
if (!string.IsNullOrWhiteSpace(database)) document.ManagementDatabase = database;
Add(document, "BizTalk Gruppenname", Value(row, "Name"), "MSBTS_GroupSetting");
Add(document, "BizTalk Administratorengruppe", Value(row, "BizTalkAdministratorGroup"), "MSBTS_GroupSetting");
Add(document, "BizTalk Operatorengruppe", Value(row, "BizTalkOperatorGroup"), "MSBTS_GroupSetting");
Add(document, "Enterprise SSO Server", Value(row, "SSOServerName"), "MSBTS_GroupSetting");
break;
}
}
}
}
catch (ManagementException exception)
{
document.Findings.Add(new Finding
{
Severity = "Hinweis",
Area = "MSBTS_GroupSetting",
Message = "Gruppenmetadaten konnten nicht vollständig gelesen werden: " + exception.Message,
RecommendedAction = "WMI-Berechtigung prüfen; die Anwendungserfassung läuft unabhängig weiter."
});
}
}
private static void AddRegistry(
InventoryDocument document,
RegistryKey key,
string valueName,
string displayName,
RegistryView view)
{
var value = Convert.ToString(key.GetValue(valueName), CultureInfo.InvariantCulture);
if (!string.IsNullOrWhiteSpace(value)
&& !document.SystemProperties.Exists(item => item.Name == displayName))
{
Add(document, displayName, value, "Registry " + view);
}
}
private static string FirstRegistry(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 row, string property)
{
try
{
return Convert.ToString(row[property], CultureInfo.InvariantCulture) ?? string.Empty;
}
catch (ManagementException)
{
return string.Empty;
}
}
private static void Add(InventoryDocument document, string name, string value, string source)
{
if (!string.IsNullOrWhiteSpace(value))
{
document.SystemProperties.Add(new NameValueRecord(name, value, source));
}
}
private static string WmiDate(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 Unknown(string value)
{
return string.IsNullOrWhiteSpace(value) ? "Nicht ermittelt" : value;
}
}
}
@@ -0,0 +1,115 @@
using System;
using System.IO;
using System.Text.RegularExpressions;
namespace BizTalkApplicationCatalog.Configuration
{
/// <summary>
/// Validiert die Kommandozeile und stellt ausschließlich normalisierte Werte bereit.
/// </summary>
internal sealed class CommandLineOptions
{
private static readonly Regex UnsafeEnvironment =
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 bool SelfTest { get; private set; }
public bool ShowHelp { get; private set; }
/// <summary>
/// Parst die Argumente. Unbekannte oder unvollständige Optionen werden abgelehnt.
/// </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 = Value(args, ref index, argument);
break;
case "--output":
result.OutputDirectory = Value(args, ref index, argument);
break;
case "--management-server":
result.ManagementServer = Value(args, ref index, argument);
break;
case "--management-database":
result.ManagementDatabase = Value(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 = UnsafeEnvironment.Replace(
result.EnvironmentName.Trim().ToUpperInvariant(), "-").Trim('-');
if (result.EnvironmentName.Length == 0)
{
throw new ArgumentException("--environment enthält keinen gültigen Namen.");
}
if (string.IsNullOrWhiteSpace(result.OutputDirectory))
{
result.OutputDirectory = Path.Combine(
Environment.CurrentDirectory, "BizTalk-Anwendungsinventar", result.EnvironmentName);
}
result.OutputDirectory = Path.GetFullPath(
Environment.ExpandEnvironmentVariables(result.OutputDirectory));
return result;
}
public static string Usage()
{
return string.Join(Environment.NewLine, new[]
{
"BEW BizTalk Application Catalog",
string.Empty,
"Aufruf:",
" BizTalkApplicationCatalog.exe --environment ACC [--output PFAD]",
" BizTalkApplicationCatalog.exe --environment PROD [--output PFAD]",
string.Empty,
"Optionen:",
" --management-server NAME Optionaler SQL-Server-Hinweis.",
" --management-database NAME Optionale Management-Datenbank.",
" --self-test Prüft XLSX-Struktur und Kernlogik ohne BizTalk.",
" --help Diese Hilfe."
});
}
private static string Value(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,41 @@
using System;
using System.Globalization;
using System.IO;
using System.Text;
namespace BizTalkApplicationCatalog.Infrastructure
{
/// <summary>
/// Spiegelt alle Fortschrittsmeldungen zeitgleich auf die Konsole und in eine UTF-8-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,61 @@
using System;
using System.Diagnostics;
using BizTalkApplicationCatalog.Models;
namespace BizTalkApplicationCatalog.Infrastructure
{
/// <summary>
/// Isoliert Erfassungsfehler, protokolliert sie und lässt unabhängige Abschnitte weiterlaufen.
/// </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;
}
public void Execute(string name, bool required, Action action)
{
var timer = Stopwatch.StartNew();
logger.Info("Starte Abschnitt: " + name);
try
{
action();
timer.Stop();
document.SectionStatuses.Add(new SectionStatus
{
Name = name,
Status = "Erfolgreich",
Message = "Abschnitt vollständig ausgeführt.",
Required = required,
DurationMilliseconds = timer.ElapsedMilliseconds
});
logger.Info("Abschnitt abgeschlossen: " + name + " (" + timer.ElapsedMilliseconds + " ms)");
}
catch (Exception exception)
{
timer.Stop();
document.SectionStatuses.Add(new SectionStatus
{
Name = name,
Status = required ? "Fehler" : "Teilweise",
Message = exception.GetType().Name + ": " + exception.Message,
Required = required,
DurationMilliseconds = timer.ElapsedMilliseconds
});
document.Findings.Add(new Finding
{
Severity = required ? "Fehler" : "Warnung",
Area = name,
Message = "Datenerfassung fehlgeschlagen: " + exception.Message,
RecommendedAction = "Logdatei, WMI-Provider und Leseberechtigungen prüfen."
});
logger.Error(name + ": " + exception.Message);
}
}
}
}
@@ -0,0 +1,190 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using BizTalkApplicationCatalog.Models;
using BizTalkApplicationCatalog.Reporting;
namespace BizTalkApplicationCatalog.Infrastructure
{
/// <summary>
/// Prüft die zentrale Berichtslogik ohne Zugriff auf Windows- oder BizTalk-WMI.
/// </summary>
internal static class SelfTestRunner
{
public static void Run(string outputPath = null)
{
Assert(SensitiveDataSanitizer.Sanitize(
"https://host/path?password=secret&client=100")
== "https://host/path?password=[REDACTED]&client=100",
"Secret-Redaktion ist fehlerhaft.");
var keepOutput = !string.IsNullOrWhiteSpace(outputPath);
var directory = keepOutput
? Path.GetDirectoryName(Path.GetFullPath(outputPath))
: Path.Combine(
Path.GetTempPath(),
"BizTalkApplicationCatalogTests-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(directory);
var path = keepOutput ? Path.GetFullPath(outputPath) : Path.Combine(directory, "test.xlsx");
try
{
var document = SampleDocument();
new XlsxReportWriter().Write(document, path);
Assert(File.Exists(path), "XLSX-Datei wurde nicht erzeugt.");
using (var archive = ZipFile.OpenRead(path))
{
var required = new[]
{
"[Content_Types].xml",
"_rels/.rels",
"docProps/core.xml",
"docProps/app.xml",
"xl/workbook.xml",
"xl/_rels/workbook.xml.rels",
"xl/styles.xml"
};
foreach (var name in required)
{
Assert(archive.GetEntry(name) != null, "XLSX-Part fehlt: " + name);
}
for (var index = 1; index <= 10; index++)
{
Assert(
archive.GetEntry("xl/worksheets/sheet" + index + ".xml") != null,
"Arbeitsblatt fehlt: " + index);
}
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);
}
}
var workbook = LoadXml(archive, "xl/workbook.xml");
XNamespace spreadsheet =
"http://schemas.openxmlformats.org/spreadsheetml/2006/main";
var names = workbook.Descendants(spreadsheet + "sheet")
.Select(item => (string)item.Attribute("name")).ToList();
Assert(names.Contains("Anwendungen"), "Anwendungsblatt fehlt.");
Assert(names.Contains("Abdeckung"), "Abdeckungsblatt fehlt.");
var contentTypes = LoadXml(archive, "[Content_Types].xml");
XNamespace contentTypeNamespace =
"http://schemas.openxmlformats.org/package/2006/content-types";
Assert(
contentTypes.Root.Elements(contentTypeNamespace + "Override").Count() == 14,
"Content-Type-Overrides sind unvollständig oder im falschen Namespace.");
var relationships = LoadXml(archive, "_rels/.rels");
XNamespace relationshipNamespace =
"http://schemas.openxmlformats.org/package/2006/relationships";
Assert(
relationships.Root.Elements(relationshipNamespace + "Relationship").Count() == 3,
"Paketbeziehungen sind unvollständig oder im falschen Namespace.");
var combinedText = new StringBuilder();
foreach (var entry in archive.Entries.Where(item =>
item.FullName.StartsWith("xl/worksheets/", StringComparison.Ordinal)))
{
using (var reader = new StreamReader(entry.Open(), Encoding.UTF8))
{
combinedText.Append(reader.ReadToEnd());
}
}
Assert(!combinedText.ToString().Contains("supersecret"), "XLSX enthält Testkennwort.");
Assert(combinedText.ToString().Contains("[REDACTED]"), "Redaktionsmarker fehlt.");
}
}
finally
{
try
{
if (!keepOutput && Directory.Exists(directory)) Directory.Delete(directory, true);
}
catch (IOException)
{
}
catch (UnauthorizedAccessException)
{
}
}
}
private static InventoryDocument SampleDocument()
{
var document = new InventoryDocument
{
EnvironmentName = "ACC",
ComputerName = "BIZTALK-ACC",
ToolVersion = "1.0.0.0",
ManagementServer = "SQL-ACC",
ManagementDatabase = "BizTalkMgmtDb",
StartedUtc = DateTime.UtcNow.AddSeconds(-1),
CompletedUtc = DateTime.UtcNow
};
document.SystemProperties.Add(new NameValueRecord(
"Betriebssystem", "Windows Server 2019", "Self-Test"));
document.Applications.Add(new ApplicationRecord
{
Name = "OrderProcessing",
Description = "Aufträge & Sonderzeichen <Test>",
Status = "Gestartet (2)",
IsDefault = "Nein",
Source = "Self-Test"
});
var port = new ArtifactRecord
{
Type = "Send Port",
ApplicationName = "OrderProcessing",
Name = "Send_Orders",
Status = "Gestartet (2)",
HostName = "SendHost",
AdapterName = "WCF-Custom",
Address = SensitiveDataSanitizer.Sanitize(
"https://service/orders?password=supersecret")
};
port.Properties.Add(new NameValueRecord("IsTwoWay", "True", "Self-Test"));
document.Artifacts.Add(port);
document.Coverage.Add(new CoverageRecord
{
DataSource = "MSBTS_Application",
Status = "Vollständig",
RowCount = 1,
Required = "Ja",
Message = "Self-Test"
});
document.SectionStatuses.Add(new SectionStatus
{
Name = "Self-Test",
Status = "Erfolgreich",
Message = "OK",
Required = true,
DurationMilliseconds = 1
});
return document;
}
private static XDocument LoadXml(ZipArchive archive, string name)
{
using (var stream = archive.GetEntry(name).Open())
{
return XDocument.Load(stream);
}
}
private static void Assert(bool condition, string message)
{
if (!condition) throw new InvalidOperationException(message);
}
}
}
@@ -0,0 +1,39 @@
using System;
using System.Text.RegularExpressions;
namespace BizTalkApplicationCatalog.Infrastructure
{
/// <summary>
/// Verhindert, dass versehentlich Kennwörter oder Token aus WMI-Textwerten in den Bericht gelangen.
/// </summary>
internal static class SensitiveDataSanitizer
{
private static readonly Regex NamedSecret = new Regex(
@"(?i)(password|passwd|pwd|secret|token|clientsecret|accesskey)\s*([=:])\s*([^;&\s""']+)",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
public static string Sanitize(string value)
{
if (string.IsNullOrEmpty(value))
{
return string.Empty;
}
return NamedSecret.Replace(value, match =>
match.Groups[1].Value + match.Groups[2].Value + "[REDACTED]");
}
public static string RedactProperty(string name, string value)
{
if (!string.IsNullOrWhiteSpace(name)
&& (name.IndexOf("password", StringComparison.OrdinalIgnoreCase) >= 0
|| name.IndexOf("secret", StringComparison.OrdinalIgnoreCase) >= 0
|| name.IndexOf("token", StringComparison.OrdinalIgnoreCase) >= 0))
{
return "[REDACTED]";
}
return Sanitize(value);
}
}
}
@@ -0,0 +1,138 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace BizTalkApplicationCatalog.Models
{
/// <summary>
/// Enthält die vollständig normalisierten Ergebnisse eines Inventarlaufs.
/// </summary>
internal sealed class InventoryDocument
{
public InventoryDocument()
{
SystemProperties = new List<NameValueRecord>();
Applications = new List<ApplicationRecord>();
Artifacts = new List<ArtifactRecord>();
Hosts = new List<HostRecord>();
Coverage = new List<CoverageRecord>();
Findings = new List<Finding>();
SectionStatuses = new List<SectionStatus>();
}
public string EnvironmentName { get; set; }
public string ComputerName { get; set; }
public string ToolVersion { get; set; }
public string ManagementServer { get; set; }
public string ManagementDatabase { get; set; }
public DateTime StartedUtc { get; set; }
public DateTime CompletedUtc { get; set; }
public List<NameValueRecord> SystemProperties { get; private set; }
public List<ApplicationRecord> Applications { get; private set; }
public List<ArtifactRecord> Artifacts { get; private set; }
public List<HostRecord> Hosts { get; private set; }
public List<CoverageRecord> Coverage { get; private set; }
public List<Finding> Findings { get; private set; }
public List<SectionStatus> SectionStatuses { get; private set; }
public bool HasRequiredFailure
{
get
{
return SectionStatuses.Any(item => item.Required && item.Status != "Erfolgreich")
|| Findings.Any(item => item.Severity == "Fehler");
}
}
}
internal sealed class ApplicationRecord
{
public string Name { get; set; }
public string Description { get; set; }
public string Status { get; set; }
public string IsDefault { get; set; }
public string Source { get; set; }
}
internal sealed class ArtifactRecord
{
public ArtifactRecord()
{
Properties = new List<NameValueRecord>();
}
public string Type { get; set; }
public string ApplicationName { get; set; }
public string Name { get; set; }
public string Status { get; set; }
public string HostName { get; set; }
public string AdapterName { get; set; }
public string ParentName { get; set; }
public string Address { get; set; }
public List<NameValueRecord> Properties { get; private set; }
public string Property(string name)
{
var match = Properties.FirstOrDefault(item =>
string.Equals(item.Name, name, StringComparison.OrdinalIgnoreCase));
return match == null ? string.Empty : match.Value;
}
}
internal sealed class HostRecord
{
public string Category { get; set; }
public string Name { get; set; }
public string Server { get; set; }
public string Status { get; set; }
public string Type { get; set; }
public string WindowsGroup { get; set; }
public string Is32BitOnly { get; set; }
public string Trusted { get; set; }
public string AdapterName { get; set; }
}
internal sealed class CoverageRecord
{
public string DataSource { get; set; }
public string Status { get; set; }
public int RowCount { get; set; }
public string Required { get; set; }
public string Message { 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; }
}
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; }
}
internal sealed class NameValueRecord
{
public NameValueRecord()
{
}
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; }
}
}
+194
View File
@@ -0,0 +1,194 @@
using System;
using System.Configuration;
using System.Globalization;
using System.IO;
using System.Linq;
using BizTalkApplicationCatalog.Collectors;
using BizTalkApplicationCatalog.Configuration;
using BizTalkApplicationCatalog.Infrastructure;
using BizTalkApplicationCatalog.Models;
using BizTalkApplicationCatalog.Reporting;
namespace BizTalkApplicationCatalog
{
/// <summary>
/// Orchestriert die read-only Erfassung, die Fortschrittsanzeige und den Excel-Export.
/// </summary>
internal static class Program
{
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 = FileName(Environment.MachineName);
var baseName = "BizTalk-Anwendungsinventar-"
+ options.EnvironmentName + "-" + safeMachine + "-" + timestamp;
var logPath = Path.Combine(options.OutputDirectory, baseName + ".log");
var reportPath = Path.Combine(options.OutputDirectory, baseName + ".xlsx");
using (var logger = new ConsoleFileLogger(logPath))
{
var document = new InventoryDocument
{
EnvironmentName = options.EnvironmentName,
ComputerName = Environment.MachineName,
StartedUtc = DateTime.UtcNow
};
logger.Info("BEW BizTalk Application Catalog startet.");
logger.Info("Umgebung: " + options.EnvironmentName);
logger.Info("Server: " + Environment.MachineName);
logger.Info("Ausgabeordner: " + options.OutputDirectory);
logger.Info("Modus: ausschließlich lesend; Excel wird ohne Office erzeugt.");
var safe = new SafeCollector(document, logger);
safe.Execute(
"System und BizTalk-Gruppe",
false,
() => new SystemCollector(options).Collect(document));
safe.Execute(
"Anwendungen und Artefakte",
true,
() => new BizTalkWmiCollector(
document,
logger,
ReadInt("WmiTimeoutSeconds", 30),
ReadInt("MaxRowsPerArtifactType", 10000)).Collect());
Evaluate(document);
document.CompletedUtc = DateTime.UtcNow;
try
{
logger.Info("Erzeuge Microsoft-Excel-Datei: " + reportPath);
new XlsxReportWriter().Write(document, reportPath);
logger.Info("Excel-Datei erfolgreich erzeugt: " + reportPath);
logger.Info("Logdatei: " + logPath);
}
catch (Exception exception)
{
logger.Error("Excel-Datei konnte nicht erzeugt werden: " + exception);
return 2;
}
logger.Info("Anwendungen: " + document.Applications.Count);
logger.Info("Artefakte: " + document.Artifacts.Count);
logger.Info("Findings: " + document.Findings.Count);
if (document.HasRequiredFailure)
{
logger.Warning("Inventar wurde mit einem Fehler in einem Pflichtabschnitt erzeugt.");
return 1;
}
logger.Info("Inventarisierung erfolgreich abgeschlossen.");
return 0;
}
}
private static void Evaluate(InventoryDocument document)
{
if (document.Applications.Count == 0)
{
document.Findings.Add(new Finding
{
Severity = "Fehler",
Area = "Anwendungen",
Message = "Keine installierte BizTalk-Anwendung ermittelt.",
RecommendedAction = "Lokal auf dem BizTalk Server mit ausreichenden WMI-Leserechten ausführen."
});
}
var withoutApplication = document.Artifacts.Count(item =>
string.IsNullOrWhiteSpace(item.ApplicationName));
if (withoutApplication > 0)
{
document.Findings.Add(new Finding
{
Severity = "Warnung",
Area = "Artefaktzuordnung",
Message = withoutApplication + " Artefakt(e) besitzen keine von WMI gelieferte Anwendungszuordnung.",
RecommendedAction = "Artefakte im Blatt 'Artefakte' prüfen und bei Bedarf mit der BizTalk Administration Console abgleichen."
});
}
if (document.Artifacts.Count == 0)
{
document.Findings.Add(new Finding
{
Severity = "Warnung",
Area = "Artefakte",
Message = "Keine Artefaktdetails wurden ermittelt; die Anwendungsliste kann dennoch vollständig sein.",
RecommendedAction = "Blatt 'Abdeckung' sowie WMI-Klassen und Berechtigungen prüfen."
});
}
}
private static int RunSelfTest()
{
try
{
SelfTestRunner.Run();
Console.WriteLine("Self-Test erfolgreich: XLSX-Paket, Tabellen und Secret-Redaktion sind gültig.");
return 0;
}
catch (Exception exception)
{
Console.Error.WriteLine("SELF-TEST FEHLGESCHLAGEN: " + exception);
return 1;
}
}
private static int ReadInt(string key, int fallback)
{
int value;
return int.TryParse(
ConfigurationManager.AppSettings[key],
NumberStyles.Integer,
CultureInfo.InvariantCulture,
out value)
? value
: fallback;
}
private static string FileName(string value)
{
var invalid = Path.GetInvalidFileNameChars();
return new string((value ?? "SERVER").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 Application Catalog")]
[assembly: AssemblyDescription("Read-only BizTalk 2020 application inventory with Microsoft Excel output")]
[assembly: AssemblyCompany("JR IT Services")]
[assembly: AssemblyProduct("BEW BizTalk Application Catalog")]
[assembly: AssemblyCopyright("Copyright © 2026")]
[assembly: ComVisible(false)]
[assembly: Guid("41cb5701-3fbc-49f4-856a-6ae930b8513d")]
[assembly: InternalsVisibleTo("BizTalkApplicationCatalog.Tests")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
@@ -0,0 +1,793 @@
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 BizTalkApplicationCatalog.Infrastructure;
using BizTalkApplicationCatalog.Models;
namespace BizTalkApplicationCatalog.Reporting
{
/// <summary>
/// Erzeugt eine filterbare Microsoft-Excel-Arbeitsmappe direkt als Office Open XML.
/// Excel oder eine Office-Interop-Installation werden nicht benötigt.
/// </summary>
internal sealed class XlsxReportWriter
{
private const string SpreadsheetNamespace =
"http://schemas.openxmlformats.org/spreadsheetml/2006/main";
private const string RelationshipsNamespace =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships";
/// <summary>
/// Schreibt atomar: Erst nach erfolgreichem Abschluss ersetzt die temporäre Datei das Ziel.
/// </summary>
public void Write(InventoryDocument document, string outputPath)
{
if (document == null) throw new ArgumentNullException("document");
if (string.IsNullOrWhiteSpace(outputPath)) throw new ArgumentException("Ausgabepfad fehlt.");
var fullPath = Path.GetFullPath(outputPath);
var directory = Path.GetDirectoryName(fullPath);
if (!string.IsNullOrWhiteSpace(directory)) Directory.CreateDirectory(directory);
var temporaryPath = fullPath + "." + Guid.NewGuid().ToString("N") + ".tmp";
try
{
var sheets = BuildSheets(document);
using (var archive = ZipFile.Open(temporaryPath, ZipArchiveMode.Create))
{
WriteContentTypes(archive, sheets.Count);
WritePackageRelationships(archive);
WriteCoreProperties(archive, document);
WriteApplicationProperties(archive);
WriteWorkbook(archive, sheets);
WriteWorkbookRelationships(archive, sheets.Count);
WriteStyles(archive);
for (var index = 0; index < sheets.Count; index++)
{
WriteWorksheet(archive, index + 1, sheets[index]);
}
}
if (File.Exists(fullPath)) File.Delete(fullPath);
File.Move(temporaryPath, fullPath);
}
finally
{
if (File.Exists(temporaryPath)) File.Delete(temporaryPath);
}
}
internal static List<SheetDefinition> BuildSheets(InventoryDocument document)
{
return new List<SheetDefinition>
{
BuildOverview(document),
BuildApplications(document),
BuildArtifacts("Artefakte", document.Artifacts),
BuildArtifacts("Ports", document.Artifacts.Where(item =>
item.Type == "Send Port"
|| item.Type == "Send Port Group"
|| item.Type == "Receive Port"
|| item.Type == "Receive Location")),
BuildArtifacts("Orchestrierungen", document.Artifacts.Where(item =>
item.Type == "Orchestrierung")),
BuildArtifacts("Schemas-Maps-Pipelines", document.Artifacts.Where(item =>
item.Type == "Schema" || item.Type == "Map" || item.Type == "Pipeline")),
BuildArtifacts("Assemblies", document.Artifacts.Where(item =>
item.Type == "Assembly")),
BuildHosts(document),
BuildCoverage(document),
BuildFindings(document)
};
}
private static SheetDefinition BuildOverview(InventoryDocument document)
{
var rows = new List<object[]>
{
new object[] { "BEW BizTalk Application Catalog", "" },
new object[] { "Umgebung", document.EnvironmentName },
new object[] { "BizTalk Server", document.ComputerName },
new object[] { "Erzeugt (lokal)", document.CompletedUtc.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture) },
new object[] { "Toolversion", document.ToolVersion },
new object[] { "Management SQL Server", Empty(document.ManagementServer) },
new object[] { "Management Database", Empty(document.ManagementDatabase) },
new object[] { "Anwendungen gesamt", document.Applications.Count },
new object[] { "Artefakte gesamt", document.Artifacts.Count },
new object[] { "Findings", document.Findings.Count },
new object[] { "", "" },
new object[] { "Artefakttyp", "Anzahl" }
};
foreach (var group in document.Artifacts
.GroupBy(item => item.Type)
.OrderBy(item => item.Key, StringComparer.OrdinalIgnoreCase))
{
rows.Add(new object[] { group.Key, group.Count() });
}
rows.Add(new object[] { "", "" });
rows.Add(new object[] { "Systemparameter", "Wert", "Quelle" });
foreach (var property in document.SystemProperties)
{
rows.Add(new object[] { property.Name, property.Value, property.Source });
}
rows.Add(new object[] { "", "" });
rows.Add(new object[] { "Erfassungsabschnitt", "Status", "Pflicht", "Dauer (ms)", "Meldung" });
foreach (var section in document.SectionStatuses)
{
rows.Add(new object[]
{
section.Name,
section.Status,
section.Required ? "Ja" : "Nein",
section.DurationMilliseconds,
section.Message
});
}
return new SheetDefinition("Übersicht", rows, false, 0);
}
private static SheetDefinition BuildApplications(InventoryDocument document)
{
var headers = new object[]
{
"Umgebung", "Server", "Anwendung", "Status", "Standard", "Beschreibung",
"Artefakte gesamt", "Orchestrierungen", "Send Ports", "Send Port Groups",
"Receive Ports", "Receive Locations", "Assemblies", "Schemas", "Maps",
"Pipelines", "Hosts", "Adapter", "Detailabdeckung"
};
var rows = new List<object[]> { headers };
var coverage = ArtifactCoverage(document);
foreach (var application in document.Applications)
{
var artifacts = document.Artifacts.Where(item =>
string.Equals(item.ApplicationName, application.Name, StringComparison.OrdinalIgnoreCase)).ToList();
rows.Add(new object[]
{
document.EnvironmentName,
document.ComputerName,
application.Name,
application.Status,
application.IsDefault,
application.Description,
artifacts.Count,
Count(artifacts, "Orchestrierung"),
Count(artifacts, "Send Port"),
Count(artifacts, "Send Port Group"),
Count(artifacts, "Receive Port"),
Count(artifacts, "Receive Location"),
Count(artifacts, "Assembly"),
Count(artifacts, "Schema"),
Count(artifacts, "Map"),
Count(artifacts, "Pipeline"),
JoinDistinct(artifacts.Select(item => item.HostName)),
JoinDistinct(artifacts.Select(item => item.AdapterName)),
coverage
});
}
return new SheetDefinition("Anwendungen", rows, true, 1);
}
private static SheetDefinition BuildArtifacts(
string sheetName,
IEnumerable<ArtifactRecord> source)
{
var rows = new List<object[]>
{
new object[]
{
"Anwendung", "Typ", "Name", "Status", "Host/Handler", "Adapter",
"Übergeordnet", "Adresse", "Beschreibung", "Two-Way", "Dynamisch",
"Deaktiviert", "Receive Pipeline", "Send Pipeline", "Secondary Adapter",
"Secondary Adresse", "Assembly/FullName", "Namespace", "Root", "Tracking"
}
};
foreach (var item in source)
{
rows.Add(new object[]
{
item.ApplicationName,
item.Type,
item.Name,
item.Status,
item.HostName,
item.AdapterName,
item.ParentName,
item.Address,
item.Property("Description"),
item.Property("IsTwoWay"),
item.Property("IsDynamic"),
item.Property("IsDisabled"),
item.Property("ReceivePipeline"),
item.Property("SendPipeline"),
item.Property("STTransportType"),
SensitiveDataSanitizer.Sanitize(item.Property("STAddress")),
FirstNonEmpty(item.Property("AssemblyName"), item.Property("FullName")),
item.Property("TargetNameSpace"),
item.Property("RootName"),
item.Property("Tracking")
});
}
return new SheetDefinition(sheetName, rows, true, 1);
}
private static SheetDefinition BuildHosts(InventoryDocument document)
{
var rows = new List<object[]>
{
new object[]
{
"Kategorie", "Name", "Server", "Status", "Typ", "Windows-Gruppe",
"Nur 32 Bit", "Vertrauenswürdig", "Adapter"
}
};
foreach (var item in document.Hosts)
{
rows.Add(new object[]
{
item.Category, item.Name, item.Server, item.Status, item.Type,
item.WindowsGroup, item.Is32BitOnly, item.Trusted, item.AdapterName
});
}
return new SheetDefinition("Hosts-Handler", rows, true, 1);
}
private static SheetDefinition BuildCoverage(InventoryDocument document)
{
var rows = new List<object[]>
{
new object[] { "Datenquelle", "Status", "Zeilen", "Pflicht", "Meldung" }
};
foreach (var item in document.Coverage)
{
rows.Add(new object[]
{
item.DataSource, item.Status, item.RowCount, item.Required, item.Message
});
}
return new SheetDefinition("Abdeckung", rows, true, 1);
}
private static SheetDefinition BuildFindings(InventoryDocument document)
{
var rows = new List<object[]>
{
new object[] { "Schweregrad", "Bereich", "Feststellung", "Empfohlene Aktion" }
};
foreach (var item in document.Findings)
{
rows.Add(new object[]
{
item.Severity, item.Area, item.Message, item.RecommendedAction
});
}
if (document.Findings.Count == 0)
{
rows.Add(new object[] { "Information", "Gesamt", "Keine Findings.", "" });
}
return new SheetDefinition("Findings", rows, true, 1);
}
private static string ArtifactCoverage(InventoryDocument document)
{
var incomplete = document.Coverage
.Where(item => item.DataSource.StartsWith("MSBTS_", StringComparison.Ordinal)
&& item.DataSource != "MSBTS_Application"
&& item.Status != "Vollständig")
.Select(item => item.DataSource + ": " + item.Status)
.ToList();
return incomplete.Count == 0
? "Vollständig"
: "Teilweise siehe Abdeckung: " + string.Join(", ", incomplete);
}
private static int Count(IEnumerable<ArtifactRecord> artifacts, string type)
{
return artifacts.Count(item => item.Type == type);
}
private static string JoinDistinct(IEnumerable<string> values)
{
return string.Join(
", ",
values.Where(item => !string.IsNullOrWhiteSpace(item))
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(item => item, StringComparer.OrdinalIgnoreCase));
}
private static string FirstNonEmpty(params string[] values)
{
return values.FirstOrDefault(item => !string.IsNullOrWhiteSpace(item)) ?? string.Empty;
}
private static string Empty(string value)
{
return string.IsNullOrWhiteSpace(value) ? "Nicht ermittelt" : value;
}
private static void WriteWorksheet(ZipArchive archive, int sheetNumber, SheetDefinition sheet)
{
using (var writer = CreateXmlWriter(archive, "xl/worksheets/sheet" + sheetNumber + ".xml"))
{
writer.WriteStartDocument();
writer.WriteStartElement("worksheet", SpreadsheetNamespace);
writer.WriteAttributeString("xmlns", "r", null, RelationshipsNamespace);
WriteSheetViews(writer, sheet.FreezeRows);
WriteColumns(writer, sheet);
writer.WriteStartElement("sheetData", SpreadsheetNamespace);
for (var rowIndex = 0; rowIndex < sheet.Rows.Count; rowIndex++)
{
writer.WriteStartElement("row", SpreadsheetNamespace);
writer.WriteAttributeString("r", (rowIndex + 1).ToString(CultureInfo.InvariantCulture));
for (var columnIndex = 0; columnIndex < sheet.Rows[rowIndex].Length; columnIndex++)
{
var style = RowStyle(sheet, rowIndex);
WriteCell(
writer,
columnIndex + 1,
rowIndex + 1,
sheet.Rows[rowIndex][columnIndex],
style);
}
writer.WriteEndElement();
}
writer.WriteEndElement();
if (sheet.AutoFilter && sheet.Rows.Count > 0 && sheet.MaximumColumns > 0)
{
writer.WriteStartElement("autoFilter", SpreadsheetNamespace);
writer.WriteAttributeString(
"ref",
"A1:" + ColumnName(sheet.MaximumColumns) + sheet.Rows.Count);
writer.WriteEndElement();
}
writer.WriteStartElement("pageMargins", SpreadsheetNamespace);
writer.WriteAttributeString("left", "0.25");
writer.WriteAttributeString("right", "0.25");
writer.WriteAttributeString("top", "0.5");
writer.WriteAttributeString("bottom", "0.5");
writer.WriteAttributeString("header", "0.2");
writer.WriteAttributeString("footer", "0.2");
writer.WriteEndElement();
writer.WriteEndElement();
writer.WriteEndDocument();
}
}
private static int RowStyle(SheetDefinition sheet, int rowIndex)
{
if (sheet.Name == "Übersicht")
{
if (rowIndex == 0) return 2;
var first = Convert.ToString(sheet.Rows[rowIndex].FirstOrDefault(), CultureInfo.InvariantCulture);
if (first == "Artefakttyp"
|| first == "Systemparameter"
|| first == "Erfassungsabschnitt") return 1;
return 0;
}
return rowIndex == 0 ? 1 : 0;
}
private static void WriteSheetViews(XmlWriter writer, int freezeRows)
{
writer.WriteStartElement("sheetViews", SpreadsheetNamespace);
writer.WriteStartElement("sheetView", SpreadsheetNamespace);
writer.WriteAttributeString("workbookViewId", "0");
if (freezeRows > 0)
{
writer.WriteStartElement("pane", SpreadsheetNamespace);
writer.WriteAttributeString("ySplit", freezeRows.ToString(CultureInfo.InvariantCulture));
writer.WriteAttributeString("topLeftCell", "A" + (freezeRows + 1));
writer.WriteAttributeString("activePane", "bottomLeft");
writer.WriteAttributeString("state", "frozen");
writer.WriteEndElement();
}
writer.WriteEndElement();
writer.WriteEndElement();
}
private static void WriteColumns(XmlWriter writer, SheetDefinition sheet)
{
writer.WriteStartElement("cols", SpreadsheetNamespace);
for (var columnIndex = 0; columnIndex < sheet.MaximumColumns; columnIndex++)
{
var width = 10;
foreach (var row in sheet.Rows)
{
if (columnIndex >= row.Length) continue;
var length = Convert.ToString(row[columnIndex], CultureInfo.InvariantCulture).Length + 2;
width = Math.Max(width, Math.Min(60, length));
}
writer.WriteStartElement("col", SpreadsheetNamespace);
writer.WriteAttributeString("min", (columnIndex + 1).ToString(CultureInfo.InvariantCulture));
writer.WriteAttributeString("max", (columnIndex + 1).ToString(CultureInfo.InvariantCulture));
writer.WriteAttributeString("width", width.ToString(CultureInfo.InvariantCulture));
writer.WriteAttributeString("customWidth", "1");
writer.WriteEndElement();
}
writer.WriteEndElement();
}
private static void WriteCell(
XmlWriter writer,
int column,
int row,
object value,
int style)
{
writer.WriteStartElement("c", SpreadsheetNamespace);
writer.WriteAttributeString("r", ColumnName(column) + row);
if (style > 0) writer.WriteAttributeString("s", style.ToString(CultureInfo.InvariantCulture));
if (IsNumber(value))
{
writer.WriteStartElement("v", SpreadsheetNamespace);
writer.WriteString(Convert.ToString(value, CultureInfo.InvariantCulture));
writer.WriteEndElement();
}
else
{
writer.WriteAttributeString("t", "inlineStr");
writer.WriteStartElement("is", SpreadsheetNamespace);
writer.WriteStartElement("t", SpreadsheetNamespace);
writer.WriteAttributeString("xml", "space", null, "preserve");
writer.WriteString(ExcelText(Convert.ToString(value, CultureInfo.InvariantCulture)));
writer.WriteEndElement();
writer.WriteEndElement();
}
writer.WriteEndElement();
}
private static bool IsNumber(object value)
{
return value is byte || value is short || value is int || value is long
|| value is float || value is double || value is decimal;
}
private static string ExcelText(string value)
{
if (string.IsNullOrEmpty(value)) return string.Empty;
var builder = new StringBuilder(Math.Min(value.Length, 32767));
foreach (var character in value)
{
if (builder.Length >= 32767) break;
if (XmlConvert.IsXmlChar(character)) builder.Append(character);
}
return builder.ToString();
}
private static string ColumnName(int number)
{
var result = string.Empty;
while (number > 0)
{
number--;
result = (char)('A' + (number % 26)) + result;
number /= 26;
}
return result;
}
private static void WriteContentTypes(ZipArchive archive, int sheetCount)
{
using (var writer = CreateXmlWriter(archive, "[Content_Types].xml"))
{
writer.WriteStartDocument();
writer.WriteStartElement("Types", "http://schemas.openxmlformats.org/package/2006/content-types");
WriteDefault(writer, "rels", "application/vnd.openxmlformats-package.relationships+xml");
WriteDefault(writer, "xml", "application/xml");
WriteOverride(writer, "/xl/workbook.xml", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml");
WriteOverride(writer, "/xl/styles.xml", "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml");
WriteOverride(writer, "/docProps/core.xml", "application/vnd.openxmlformats-package.core-properties+xml");
WriteOverride(writer, "/docProps/app.xml", "application/vnd.openxmlformats-officedocument.extended-properties+xml");
for (var index = 1; index <= sheetCount; index++)
{
WriteOverride(
writer,
"/xl/worksheets/sheet" + index + ".xml",
"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml");
}
writer.WriteEndElement();
writer.WriteEndDocument();
}
}
private static void WriteDefault(XmlWriter writer, string extension, string type)
{
writer.WriteStartElement("Default");
writer.WriteAttributeString("Extension", extension);
writer.WriteAttributeString("ContentType", type);
writer.WriteEndElement();
}
private static void WriteOverride(XmlWriter writer, string partName, string type)
{
writer.WriteStartElement("Override");
writer.WriteAttributeString("PartName", partName);
writer.WriteAttributeString("ContentType", type);
writer.WriteEndElement();
}
private static void WritePackageRelationships(ZipArchive archive)
{
using (var writer = CreateXmlWriter(archive, "_rels/.rels"))
{
writer.WriteStartDocument();
writer.WriteStartElement("Relationships", "http://schemas.openxmlformats.org/package/2006/relationships");
WriteRelationship(writer, "rId1", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument", "xl/workbook.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 WriteWorkbook(ZipArchive archive, List<SheetDefinition> sheets)
{
using (var writer = CreateXmlWriter(archive, "xl/workbook.xml"))
{
writer.WriteStartDocument();
writer.WriteStartElement("workbook", SpreadsheetNamespace);
writer.WriteAttributeString("xmlns", "r", null, RelationshipsNamespace);
writer.WriteStartElement("bookViews", SpreadsheetNamespace);
writer.WriteStartElement("workbookView", SpreadsheetNamespace);
writer.WriteAttributeString("activeTab", "0");
writer.WriteEndElement();
writer.WriteEndElement();
writer.WriteStartElement("sheets", SpreadsheetNamespace);
for (var index = 0; index < sheets.Count; index++)
{
writer.WriteStartElement("sheet", SpreadsheetNamespace);
writer.WriteAttributeString("name", sheets[index].Name);
writer.WriteAttributeString("sheetId", (index + 1).ToString(CultureInfo.InvariantCulture));
writer.WriteAttributeString("r", "id", RelationshipsNamespace, "rId" + (index + 1));
writer.WriteEndElement();
}
writer.WriteEndElement();
writer.WriteEndElement();
writer.WriteEndDocument();
}
}
private static void WriteWorkbookRelationships(ZipArchive archive, int sheetCount)
{
using (var writer = CreateXmlWriter(archive, "xl/_rels/workbook.xml.rels"))
{
writer.WriteStartDocument();
writer.WriteStartElement("Relationships", "http://schemas.openxmlformats.org/package/2006/relationships");
for (var index = 1; index <= sheetCount; index++)
{
WriteRelationship(
writer,
"rId" + index,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet",
"worksheets/sheet" + index + ".xml");
}
WriteRelationship(
writer,
"rId" + (sheetCount + 1),
"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");
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, "xl/styles.xml"))
{
writer.WriteStartDocument();
writer.WriteStartElement("styleSheet", SpreadsheetNamespace);
writer.WriteStartElement("fonts", SpreadsheetNamespace);
writer.WriteAttributeString("count", "3");
WriteFont(writer, false, "000000", 10);
WriteFont(writer, true, "FFFFFF", 10);
WriteFont(writer, true, "FFFFFF", 16);
writer.WriteEndElement();
writer.WriteStartElement("fills", SpreadsheetNamespace);
writer.WriteAttributeString("count", "4");
WritePatternFill(writer, "none", null);
WritePatternFill(writer, "gray125", null);
WritePatternFill(writer, "solid", "1F4E78");
WritePatternFill(writer, "solid", "2F75B5");
writer.WriteEndElement();
writer.WriteStartElement("borders", SpreadsheetNamespace);
writer.WriteAttributeString("count", "2");
WriteBorder(writer, false);
WriteBorder(writer, true);
writer.WriteEndElement();
writer.WriteStartElement("cellStyleXfs", SpreadsheetNamespace);
writer.WriteAttributeString("count", "1");
WriteXf(writer, 0, 0, 0, false);
writer.WriteEndElement();
writer.WriteStartElement("cellXfs", SpreadsheetNamespace);
writer.WriteAttributeString("count", "3");
WriteXf(writer, 0, 0, 0, true);
WriteXf(writer, 1, 2, 1, true);
WriteXf(writer, 2, 3, 1, true);
writer.WriteEndElement();
writer.WriteStartElement("cellStyles", SpreadsheetNamespace);
writer.WriteAttributeString("count", "1");
writer.WriteStartElement("cellStyle", SpreadsheetNamespace);
writer.WriteAttributeString("name", "Normal");
writer.WriteAttributeString("xfId", "0");
writer.WriteAttributeString("builtinId", "0");
writer.WriteEndElement();
writer.WriteEndElement();
writer.WriteEndElement();
writer.WriteEndDocument();
}
}
private static void WriteFont(XmlWriter writer, bool bold, string color, int size)
{
writer.WriteStartElement("font", SpreadsheetNamespace);
if (bold) writer.WriteElementString("b", SpreadsheetNamespace, string.Empty);
writer.WriteStartElement("sz", SpreadsheetNamespace);
writer.WriteAttributeString("val", size.ToString(CultureInfo.InvariantCulture));
writer.WriteEndElement();
writer.WriteStartElement("color", SpreadsheetNamespace);
writer.WriteAttributeString("rgb", "FF" + color);
writer.WriteEndElement();
writer.WriteStartElement("name", SpreadsheetNamespace);
writer.WriteAttributeString("val", "Calibri");
writer.WriteEndElement();
writer.WriteEndElement();
}
private static void WritePatternFill(XmlWriter writer, string pattern, string color)
{
writer.WriteStartElement("fill", SpreadsheetNamespace);
writer.WriteStartElement("patternFill", SpreadsheetNamespace);
writer.WriteAttributeString("patternType", pattern);
if (color != null)
{
writer.WriteStartElement("fgColor", SpreadsheetNamespace);
writer.WriteAttributeString("rgb", "FF" + color);
writer.WriteEndElement();
writer.WriteStartElement("bgColor", SpreadsheetNamespace);
writer.WriteAttributeString("indexed", "64");
writer.WriteEndElement();
}
writer.WriteEndElement();
writer.WriteEndElement();
}
private static void WriteBorder(XmlWriter writer, bool thin)
{
writer.WriteStartElement("border", SpreadsheetNamespace);
foreach (var side in new[] { "left", "right", "top", "bottom" })
{
writer.WriteStartElement(side, SpreadsheetNamespace);
if (thin)
{
writer.WriteAttributeString("style", "thin");
writer.WriteStartElement("color", SpreadsheetNamespace);
writer.WriteAttributeString("rgb", "FFD9E2F3");
writer.WriteEndElement();
}
writer.WriteEndElement();
}
writer.WriteElementString("diagonal", SpreadsheetNamespace, string.Empty);
writer.WriteEndElement();
}
private static void WriteXf(
XmlWriter writer,
int fontId,
int fillId,
int borderId,
bool alignment)
{
writer.WriteStartElement("xf", SpreadsheetNamespace);
writer.WriteAttributeString("numFmtId", "0");
writer.WriteAttributeString("fontId", fontId.ToString(CultureInfo.InvariantCulture));
writer.WriteAttributeString("fillId", fillId.ToString(CultureInfo.InvariantCulture));
writer.WriteAttributeString("borderId", borderId.ToString(CultureInfo.InvariantCulture));
writer.WriteAttributeString("xfId", "0");
if (fontId > 0) writer.WriteAttributeString("applyFont", "1");
if (fillId > 0) writer.WriteAttributeString("applyFill", "1");
if (borderId > 0) writer.WriteAttributeString("applyBorder", "1");
if (alignment)
{
writer.WriteAttributeString("applyAlignment", "1");
writer.WriteStartElement("alignment", SpreadsheetNamespace);
writer.WriteAttributeString("vertical", "top");
writer.WriteAttributeString("wrapText", "1");
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-Anwendungsinventar " + document.EnvironmentName);
writer.WriteElementString("dc", "creator", "http://purl.org/dc/elements/1.1/", "BEW BizTalk Application Catalog");
writer.WriteElementString("cp", "lastModifiedBy", "http://schemas.openxmlformats.org/package/2006/metadata/core-properties", "BEW BizTalk Application Catalog");
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("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture));
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", "http://schemas.openxmlformats.org/officeDocument/2006/extended-properties");
writer.WriteAttributeString("xmlns", "vt", null, "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes");
writer.WriteElementString("Application", "BEW BizTalk Application Catalog");
writer.WriteElementString("AppVersion", "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
});
}
internal sealed class SheetDefinition
{
public SheetDefinition(
string name,
List<object[]> rows,
bool autoFilter,
int freezeRows)
{
Name = name;
Rows = rows;
AutoFilter = autoFilter;
FreezeRows = freezeRows;
}
public string Name { get; private set; }
public List<object[]> Rows { get; private set; }
public bool AutoFilter { get; private set; }
public int FreezeRows { get; private set; }
public int MaximumColumns
{
get { return Rows.Count == 0 ? 0 : Rows.Max(item => item.Length); }
}
}
}
}
@@ -0,0 +1,49 @@
<?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>{6B7CE874-8054-46F5-927C-E9B26927F187}</ProjectGuid>
<OutputType>Exe</OutputType>
<RootNamespace>BizTalkApplicationCatalog.Tests</RootNamespace>
<AssemblyName>BizTalkApplicationCatalog.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</DefineConstants>
<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>
<WarningLevel>4</WarningLevel>
<PlatformTarget>AnyCPU</PlatformTarget>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
</ItemGroup>
<ItemGroup>
<Compile Include="Program.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\BizTalkApplicationCatalog\BizTalkApplicationCatalog.csproj">
<Project>{41CB5701-3FBC-49F4-856A-6AE930B8513D}</Project>
<Name>BizTalkApplicationCatalog</Name>
<Private>True</Private>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
@@ -0,0 +1,23 @@
using System;
using BizTalkApplicationCatalog.Infrastructure;
namespace BizTalkApplicationCatalog.Tests
{
internal static class Program
{
private static int Main(string[] args)
{
try
{
SelfTestRunner.Run(args.Length > 0 ? args[0] : null);
Console.WriteLine("Alle Tests erfolgreich.");
return 0;
}
catch (Exception exception)
{
Console.Error.WriteLine("TEST FEHLGESCHLAGEN: " + exception);
return 1;
}
}
}
}