Reduce catalog to Phase 1 ACC/PROD view
This commit is contained in:
+213
-193
@@ -1,220 +1,240 @@
|
||||
# Technische Dokumentation: BEW BizTalk Application Catalog
|
||||
# Technische Dokumentation: BEW BizTalk Application Catalog – Phase 1
|
||||
|
||||
## 1. Zielbild
|
||||
## 1. Ziel und Abgrenzung
|
||||
|
||||
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.
|
||||
Das Werkzeug bildet ausschließlich die Phase-1-Sicht `BizTalk Anwendungen & Adapter` ab. Das fachliche Ergebnis ist ein einblättriger ACC/PRD-Katalog mit:
|
||||
|
||||
1. BizTalk-Anwendung
|
||||
2. Umgebung
|
||||
3. Adapter-Typ und Anzahl
|
||||
4. Endpunktanzahl
|
||||
5. Vorkommen in ACC
|
||||
6. Vorkommen in PRD
|
||||
7. Hinweis bei einseitigem Vorkommen
|
||||
|
||||
Es ist kein technisches Vollinventar. Frühere Detailbereiche zu System, SQL, Status, Ports, Orchestrierungen, Schemas, Maps, Pipelines, Assemblies, Hosts, Handlern, Abdeckung und Findings sind nicht Bestandteil der XLSX und werden nicht mehr erhoben.
|
||||
|
||||
## 2. Verteilte Erfassung
|
||||
|
||||
ACC und PROD besitzen jeweils einen lokalen BizTalk Server 2020. Ein Prozess auf einem Server soll die andere Umgebung nicht remote abfragen.
|
||||
|
||||
```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 BizTalk Server PROD BizTalk Server
|
||||
| |
|
||||
| lokales WMI | lokales WMI
|
||||
v v
|
||||
ACC JSON + lokale XLSX PROD JSON + lokale XLSX
|
||||
\ /
|
||||
\ /
|
||||
+---- Merge auf beliebigem -----+
|
||||
Windows-Rechner
|
||||
|
|
||||
v
|
||||
gemeinsame ACC/PRD-XLSX
|
||||
```
|
||||
|
||||
ACC und PROD werden getrennt inventarisiert. Da es pro Umgebung nur einen BizTalk-Knoten gibt, ist kein Node-Abgleich erforderlich.
|
||||
Der JSON-Snapshot ist das kleine Austauschformat zwischen den getrennten Erfassungen. Er enthält weder Endpunktadressen noch Credentials.
|
||||
|
||||
## 2. Architektur
|
||||
## 3. Komponenten
|
||||
|
||||
```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
|
||||
CommandLineOptions
|
||||
+-- validiert lokalen Modus oder Merge-Modus
|
||||
|
||||
BizTalkWmiCollector
|
||||
+-- MSBTS_Application
|
||||
+-- MSBTS_ReceivePort
|
||||
+-- MSBTS_SendPort
|
||||
+-- MSBTS_ReceiveLocation
|
||||
|
||||
SnapshotStore
|
||||
+-- normalisiert je Anwendung Adapterzählungen
|
||||
+-- schreibt/liest DataContract-JSON
|
||||
+-- validiert Schema, Summen, Umgebung und Vollständigkeit
|
||||
|
||||
XlsxReportWriter
|
||||
+-- genau ein Blatt "Phase 1"
|
||||
+-- Office Open XML ohne Office-Interop
|
||||
+-- Titel, Filter, Freeze Pane und Summenzeile
|
||||
```
|
||||
|
||||
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.
|
||||
## 4. WMI-Datenmodell
|
||||
|
||||
## 3. Toolchain und Kompatibilität
|
||||
### 4.1 Anwendungen
|
||||
|
||||
Die Build-Baseline ist absichtlich auf die BizTalk-2020-Umgebung ausgerichtet:
|
||||
`MSBTS_Application` ist die Pflichtquelle der Anwendungsliste. Verwendet wird nur `Name` beziehungsweise `ApplicationName`.
|
||||
|
||||
- Visual Studio 2019 / Solution Version 16
|
||||
- MSBuild 16.x, insbesondere 16.11
|
||||
- klassisches MSBuild-Projektformat mit `ToolsVersion="15.0"`
|
||||
Die Abfrage muss mindestens eine Anwendung liefern. Andernfalls ist der lokale Lauf unvollständig.
|
||||
|
||||
### 4.2 Receive-Endpunkte
|
||||
|
||||
Eine `MSBTS_ReceiveLocation` zählt als ein Endpunkt. Der Adapter wird defensiv aus `AdapterName`, `TransportType` oder `PTTransportType` gelesen.
|
||||
|
||||
Da eine Receive Location abhängig vom WMI-Provider keine direkte `ApplicationName` liefern kann, wird vorher `MSBTS_ReceivePort` gelesen. Über `ReceivePortName` wird die Anwendung aufgelöst. Ein Receive Port zählt selbst nicht als Endpunkt.
|
||||
|
||||
### 4.3 Send-Endpunkte
|
||||
|
||||
Bei `MSBTS_SendPort` zählt die primäre Transportkonfiguration als ein Endpunkt. Ist eine sekundäre Transportkonfiguration vorhanden, zählt sie als zusätzlicher Endpunkt.
|
||||
|
||||
Primärer Adapter:
|
||||
|
||||
- `PTTransportType`
|
||||
- Fallback `PrimaryTransportType`
|
||||
- Fallback `AdapterName`
|
||||
- Fallback `TransportType`
|
||||
|
||||
Sekundärer Adapter:
|
||||
|
||||
- `STTransportType`
|
||||
- Fallback `SecondaryTransportType`
|
||||
|
||||
Fehlt ein gelieferter Adaptername, wird `Unbekannt` verwendet. Ein Endpunkt ohne Anwendungszuordnung macht den Lauf unvollständig, weil er sonst in der Anwendungssicht verloren ginge.
|
||||
|
||||
## 5. Snapshot-Format
|
||||
|
||||
Das JSON enthält nur normalisierte Katalogdaten:
|
||||
|
||||
```json
|
||||
{
|
||||
"SchemaVersion": 1,
|
||||
"EnvironmentName": "ACC",
|
||||
"ComputerName": "BIZTALK-ACC",
|
||||
"CreatedUtc": "2026-07-27T13:00:00.0000000Z",
|
||||
"IsComplete": true,
|
||||
"Applications": [
|
||||
{
|
||||
"Name": "ExampleApplication",
|
||||
"EndpointCount": 2,
|
||||
"AdapterCounts": [
|
||||
{ "AdapterType": "FILE", "Count": 1 },
|
||||
{ "AdapterType": "SFTP", "Count": 1 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Vor einem Merge werden geprüft:
|
||||
|
||||
- `SchemaVersion` ist `1`
|
||||
- erwartete Umgebung ist exakt `ACC` beziehungsweise `PROD`
|
||||
- `IsComplete` ist `true`
|
||||
- Namen und Adaptertypen sind nicht leer
|
||||
- Zählwerte sind nicht negativ
|
||||
- Summe der Adapterzählungen entspricht `EndpointCount`
|
||||
|
||||
JSON und XLSX werden über eine temporäre Datei geschrieben und erst nach erfolgreichem Abschluss auf den endgültigen Dateinamen verschoben.
|
||||
|
||||
## 6. Merge-Regeln
|
||||
|
||||
Anwendungsnamen werden ohne Beachtung der Groß-/Kleinschreibung verglichen. Für die Anzeige wird bei einer gemeinsamen Anwendung bevorzugt die Schreibweise aus ACC verwendet.
|
||||
|
||||
| Situation | Umgebung | ACC | PRD | Hinweis |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| in beiden vorhanden | `ACC + PRD` | `✓` | `✓` | leer |
|
||||
| nur ACC | `nur ACC` | `✓` | `–` | `Nur in ACC vorhanden` |
|
||||
| nur PROD | `nur PRD` | `–` | `✓` | `Nur in PRD vorhanden – nicht in ACC` |
|
||||
|
||||
Die Endpunktanzahl einer gemeinsamen Zeile ist die Summe beider Umgebungen.
|
||||
|
||||
Sind die Adapterzählungen identisch, erscheint nur eine kompakte Darstellung:
|
||||
|
||||
```text
|
||||
FILE (1x) / SFTP (1x)
|
||||
```
|
||||
|
||||
Bei einer Abweichung bleiben beide Nachweise sichtbar:
|
||||
|
||||
```text
|
||||
ACC: FILE (2x) | PRD: FILE (1x) / SFTP (1x)
|
||||
```
|
||||
|
||||
Damit wird keine Übereinstimmung suggeriert, wenn lediglich die Gesamtzahl gleich ist.
|
||||
|
||||
## 7. Excel-Aufbau
|
||||
|
||||
Die Arbeitsmappe enthält genau:
|
||||
|
||||
```text
|
||||
xl/worksheets/sheet1.xml -> Blatt "Phase 1"
|
||||
```
|
||||
|
||||
Das Blatt besteht aus:
|
||||
|
||||
- Zeile 1: zusammengeführter Titel
|
||||
- Zeile 2: sieben Spaltenüberschriften
|
||||
- ab Zeile 3: alphabetisch sortierte Anwendungen
|
||||
- letzte Zeile: zusammengeführte Summenzeile
|
||||
|
||||
Der Filter umfasst nur Kopf- und Datenzeilen, nicht die Summenzeile. Die ersten beiden Zeilen sind fixiert. Zelltexte werden als `inlineStr` gespeichert. Office und COM-Interop sind nicht erforderlich.
|
||||
|
||||
## 8. Fehlerverhalten
|
||||
|
||||
Alle vier WMI-Abfragen bilden einen Pflichtabschnitt. Ein Fehler führt zu:
|
||||
|
||||
- Exitcode `1`
|
||||
- Logeintrag
|
||||
- JSON mit `IsComplete: false`
|
||||
- lokaler XLSX auf Basis der bis dahin ermittelten Daten
|
||||
|
||||
Der Merge lehnt einen solchen JSON-Snapshot ab. Dadurch werden technische Lücken nicht als bestätigte Nullwerte in den ACC/PRD-Katalog übernommen.
|
||||
|
||||
Ungültige Parameter, fehlende Dateien, fehlerhaftes JSON und Schreibfehler liefern Exitcode `2`.
|
||||
|
||||
## 9. Sicherheit
|
||||
|
||||
- ausschließlich lesende lokale WMI-Abfragen
|
||||
- keine Änderungen an BizTalk-Konfiguration oder Runtime
|
||||
- keine SQL-Abfragen
|
||||
- keine Endpunktadressen
|
||||
- keine Binding-Exporte
|
||||
- keine Credentials, Secrets oder Tokens
|
||||
- keine Netzwerkverbindungen zu fachlichen Endpunkten
|
||||
|
||||
Anwendungsnamen, Servernamen und Adapterverteilungen bleiben interne Betriebsinformationen. JSON, XLSX und Log sind geschützt abzulegen.
|
||||
|
||||
## 10. Build und Test
|
||||
|
||||
Baseline:
|
||||
|
||||
- Visual Studio 2019 / MSBuild 16.x
|
||||
- klassisches MSBuild-Projektformat
|
||||
- .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:
|
||||
- keine NuGet-Pakete
|
||||
- kein .NET SDK erforderlich
|
||||
|
||||
```cmd
|
||||
scripts\build-release.cmd
|
||||
```
|
||||
|
||||
Der Build:
|
||||
Der Self-Test benötigt kein BizTalk und prüft:
|
||||
|
||||
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.
|
||||
- JSON-Schreiben und -Lesen
|
||||
- Snapshot-Validierung
|
||||
- ACC/PRD-Union
|
||||
- Adapteraggregation
|
||||
- Endpunktsummen
|
||||
- ein einziges Blatt `Phase 1`
|
||||
- erforderliche OPC-/XLSX-Teile
|
||||
- XML-Wohlgeformtheit
|
||||
- Titel- und Summen-Merges
|
||||
- korrekten Filterbereich
|
||||
|
||||
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
|
||||
## 11. Betriebsvalidierung
|
||||
|
||||
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.
|
||||
1. `--self-test` erfolgreich ausführen.
|
||||
2. Lokalen Lauf in administrativer `cmd.exe` starten.
|
||||
3. Exitcode `0` und Log prüfen.
|
||||
4. Anwendungsgesamtzahl mit der BizTalk Administration Console vergleichen.
|
||||
5. Send Ports, sekundäre Transporte und Receive Locations stichprobenartig zählen.
|
||||
6. Nur die vollständige JSON-Datei für den Merge verwenden.
|
||||
|
||||
ACC- und PROD-Dateien dürfen nicht zusammengeführt werden, ohne die Spalten `Umgebung` und `Server` beizubehalten.
|
||||
Nach dem Merge:
|
||||
|
||||
1. Summen für ACC und PROD mit den lokalen Dateien vergleichen.
|
||||
2. Anwendungen mit `nur ACC` und `nur PRD` fachlich prüfen.
|
||||
3. Abweichend beschriftete ACC-/PRD-Adapterverteilungen prüfen.
|
||||
4. Ergebnis geschützt archivieren.
|
||||
|
||||
@@ -1,82 +1,61 @@
|
||||
# BEW BizTalk Application Catalog
|
||||
# BEW BizTalk Application Catalog – Phase 1
|
||||
|
||||
`BEW BizTalk Application Catalog` inventarisiert die auf einem BizTalk Server 2020 installierten Anwendungen und erzeugt eine kompakte, filterbare Microsoft-Excel-Arbeitsmappe (`.xlsx`).
|
||||
Das Tool erzeugt nur die für die Phase-1-Excel-Sicht benötigten Daten:
|
||||
|
||||
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 |
|
||||
| Spalte | 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 |
|
||||
| `BizTalk-Anwendung` | Name der BizTalk-Anwendung |
|
||||
| `Umgebung` | `ACC + PRD`, `nur ACC` oder `nur PRD` |
|
||||
| `Adapter-Typ` | Adapter mit Anzahl, zum Beispiel `FILE (2x) / SFTP (1x)` |
|
||||
| `Anzahl Endpunkte` | Summe der Endpunkte in den berücksichtigten Umgebungen |
|
||||
| `BizTalk-Anwendung in ACC` | `✓` oder `–` |
|
||||
| `BizTalk-Anwendung in PRD` | `✓` oder `–` |
|
||||
| `Hinweis` | Kennzeichnung einer nur einseitig vorhandenen Anwendung |
|
||||
|
||||
Alle Detailblätter besitzen Filter, fixierte Kopfzeilen und angepasste Spaltenbreiten.
|
||||
Die Arbeitsmappe enthält exakt ein Blatt namens `Phase 1`. Es werden keine weiteren Übersichts-, Artefakt-, Port-, Host-, Abdeckungs- oder Findings-Blätter erzeugt.
|
||||
|
||||
## Kernparameter je Anwendung
|
||||
## Ablauf
|
||||
|
||||
Das Blatt `Anwendungen` enthält unter anderem:
|
||||
ACC und PROD sind getrennte BizTalk-Umgebungen. Deshalb arbeitet das Tool in zwei Schritten:
|
||||
|
||||
- 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
|
||||
1. Auf dem lokalen ACC-BizTalk-Server wird ein ACC-JSON-Snapshot erzeugt.
|
||||
2. Auf dem lokalen PROD-BizTalk-Server wird ein PROD-JSON-Snapshot erzeugt.
|
||||
3. Beide JSON-Dateien werden mit demselben Tool zur gemeinsamen Excel-Sicht zusammengeführt.
|
||||
|
||||
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.
|
||||
Jeder lokale Lauf erzeugt zusätzlich eine einblättrige Excel-Sicht für die jeweilige Umgebung. Erst der Merge liefert den belastbaren ACC/PRD-Vergleich wie in der Zielansicht.
|
||||
|
||||
## Sicherheits- und Betriebsprinzip
|
||||
## Erfasste Daten
|
||||
|
||||
- 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
|
||||
Das Tool liest ausschließlich:
|
||||
|
||||
Die XLSX- und Logdatei enthält interne Server-, Anwendungs-, Host- und Endpunktinformationen und muss entsprechend geschützt abgelegt werden.
|
||||
- `MSBTS_Application` für die vollständige Anwendungsliste
|
||||
- `MSBTS_SendPort` für primäre und gegebenenfalls sekundäre Send-Endpunkte
|
||||
- `MSBTS_ReceivePort` nur zur Zuordnung einer Receive Location zur Anwendung
|
||||
- `MSBTS_ReceiveLocation` für Receive-Endpunkte
|
||||
|
||||
## Voraussetzungen auf ACC und PROD
|
||||
Ein Endpunkt ist:
|
||||
|
||||
- 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
|
||||
- die primäre Transportkonfiguration eines Send Ports,
|
||||
- eine konfigurierte sekundäre Transportkonfiguration eines Send Ports oder
|
||||
- eine Receive Location.
|
||||
|
||||
Receive Ports selbst sind Container und werden nicht als Endpunkt gezählt. Anwendungen ohne Endpunkt bleiben mit `0` und Adapter-Typ `–` in der Sicht enthalten.
|
||||
|
||||
Nicht erfasst werden insbesondere Adressen, Credentials, Beschreibungen, Status, Hosts, Handler, Orchestrierungen, Schemas, Maps, Pipelines, Assemblies, Betriebssystem- oder SQL-Daten.
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
- BizTalk Server 2020 auf Windows Server 2019
|
||||
- lokale Ausführung auf dem jeweiligen BizTalk-Server
|
||||
- .NET Framework 4.7.2 oder höher
|
||||
- funktionierender lokaler BizTalk-WMI-Provider
|
||||
- Leserechte auf `root\MicrosoftBizTalkServer`
|
||||
- administrative `cmd.exe` empfohlen
|
||||
|
||||
Microsoft Office ist ausdrücklich keine Voraussetzung.
|
||||
Microsoft Excel, Microsoft Office, PowerShell, Internetzugriff, NuGet und ein .NET SDK werden auf dem Zielserver nicht benötigt.
|
||||
|
||||
## Schnellstart
|
||||
## Lokalen Snapshot erzeugen
|
||||
|
||||
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:
|
||||
Zuerst den Self-Test ausführen:
|
||||
|
||||
```cmd
|
||||
BizTalkApplicationCatalog.exe --self-test
|
||||
@@ -94,117 +73,95 @@ PROD:
|
||||
run-inventory.cmd PROD C:\BizTalk-Doku\PROD
|
||||
```
|
||||
|
||||
Ergebnis eines erfolgreichen lokalen Laufs:
|
||||
|
||||
```text
|
||||
Phase1-BizTalk-Anwendungskatalog-ACC-SERVER-20260727-150000.json
|
||||
Phase1-BizTalk-Anwendungskatalog-ACC-SERVER-20260727-150000.xlsx
|
||||
Phase1-BizTalk-Anwendungskatalog-ACC-SERVER-20260727-150000.log
|
||||
```
|
||||
|
||||
Nur JSON-Dateien eines Laufs mit Exitcode `0` dürfen zusammengeführt werden. Ein unvollständiger Snapshot trägt `IsComplete: false` und wird vom Merge abgelehnt.
|
||||
|
||||
## ACC und PROD zusammenführen
|
||||
|
||||
Beide JSON-Dateien in einen gemeinsamen, geschützten Ordner kopieren und ausführen:
|
||||
|
||||
```cmd
|
||||
run-merge.cmd ^
|
||||
C:\BizTalk-Doku\ACC\Phase1-BizTalk-Anwendungskatalog-ACC-SERVER-20260727-150000.json ^
|
||||
C:\BizTalk-Doku\PROD\Phase1-BizTalk-Anwendungskatalog-PROD-SERVER-20260727-151500.json ^
|
||||
C:\BizTalk-Doku\ACC-PROD
|
||||
```
|
||||
|
||||
Direkter Aufruf:
|
||||
|
||||
```cmd
|
||||
BizTalkApplicationCatalog.exe ^
|
||||
--environment ACC ^
|
||||
--output C:\BizTalk-Doku\ACC
|
||||
--merge ^
|
||||
--acc-json C:\BizTalk-Doku\ACC\ACC.json ^
|
||||
--prod-json C:\BizTalk-Doku\PROD\PROD.json ^
|
||||
--output C:\BizTalk-Doku\ACC-PROD
|
||||
```
|
||||
|
||||
## Konsolenausgabe
|
||||
|
||||
Das Tool zeigt laufend, was es gerade erfasst und wann die Excel-Datei geschrieben wurde:
|
||||
Die gemeinsame Datei heißt beispielsweise:
|
||||
|
||||
```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\...
|
||||
Phase1-BizTalk-Anwendungskatalog-ACC-PROD-20260727-153000.xlsx
|
||||
```
|
||||
|
||||
## Ausgabedateien
|
||||
Bei identischer Adapterverteilung in ACC und PROD wird sie einmal angezeigt. Bei Abweichungen kennzeichnet die Zelle beide Werte, zum Beispiel:
|
||||
|
||||
```text
|
||||
BizTalk-Anwendungsinventar-ACC-BIZTALKSERVER-20260727-150000.xlsx
|
||||
BizTalk-Anwendungsinventar-ACC-BIZTALKSERVER-20260727-150000.log
|
||||
ACC: FILE (2x) | PRD: FILE (1x) / SFTP (1x)
|
||||
```
|
||||
|
||||
`Anzahl Endpunkte` ist im Merge die Summe aus ACC und PROD.
|
||||
|
||||
## 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 |
|
||||
| `--environment ACC\|PROD` | lokaler Inventarlauf |
|
||||
| `--output PFAD` | Zielordner |
|
||||
| `--merge` | ACC- und PROD-Snapshot zusammenführen |
|
||||
| `--acc-json PFAD` | vollständiger ACC-Snapshot |
|
||||
| `--prod-json PFAD` | vollständiger PROD-Snapshot |
|
||||
| `--self-test` | JSON-, Merge- und XLSX-Test ohne BizTalk |
|
||||
| `--help` | Hilfe |
|
||||
|
||||
| Exitcode | Bedeutung |
|
||||
| ---: | --- |
|
||||
| `0` | vollständige Anwendungserfassung und XLSX erfolgreich |
|
||||
| `1` | XLSX erzeugt, aber ein Pflichtabschnitt war unvollständig |
|
||||
| `2` | Aufruf-, Ausgabe- oder Berichtserzeugungsfehler |
|
||||
| `0` | Snapshot beziehungsweise Merge vollständig erzeugt |
|
||||
| `1` | lokaler Pflichtabschnitt unvollständig; JSON nicht mergen |
|
||||
| `2` | Aufruf-, Datei-, JSON- oder XLSX-Fehler |
|
||||
|
||||
## Build mit Visual Studio 2019
|
||||
## Build
|
||||
|
||||
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`.
|
||||
Die Solution verwendet das klassische MSBuild-Projektformat für Visual Studio 2019 / MSBuild 16.x, .NET Framework 4.7.2 und C# 7.3. Es gibt keine NuGet-Abhängigkeiten.
|
||||
|
||||
```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:
|
||||
## Quellpaket als certutil-Text
|
||||
|
||||
```cmd
|
||||
scripts\package-source.cmd
|
||||
```
|
||||
|
||||
Erzeugt werden:
|
||||
Erzeugt:
|
||||
|
||||
```text
|
||||
artifacts\BizTalkApplicationCatalog-source.zip
|
||||
artifacts\BizTalkApplicationCatalog-source.zip.txt
|
||||
```
|
||||
|
||||
Dekodieren unter Windows:
|
||||
Dekodieren:
|
||||
|
||||
```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).
|
||||
Technische Details stehen in [Dokumentation.md](Dokumentation.md).
|
||||
|
||||
@@ -19,7 +19,7 @@ if "%~2"=="" (
|
||||
)
|
||||
|
||||
echo ============================================================
|
||||
echo BEW BizTalk Application Catalog
|
||||
echo BEW BizTalk Application Catalog - Phase 1
|
||||
echo Umgebung: %ENVIRONMENT%
|
||||
echo Server: %COMPUTERNAME%
|
||||
echo Ausgabe: %OUTPUT%
|
||||
@@ -34,7 +34,7 @@ echo.
|
||||
if "%EXITCODE%"=="0" (
|
||||
echo Inventar erfolgreich erstellt.
|
||||
) else if "%EXITCODE%"=="1" (
|
||||
echo Inventar mit Teilfehler erstellt. XLSX, Abdeckung, Findings und Log pruefen.
|
||||
echo Inventar unvollstaendig. JSON darf nicht zusammengefuehrt werden; Log pruefen.
|
||||
) else (
|
||||
echo Inventar konnte nicht erstellt werden.
|
||||
)
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
@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
|
||||
if "%~2"=="" goto :usage
|
||||
|
||||
if "%~3"=="" (
|
||||
set "OUTPUT=%CD%\BizTalk-Anwendungskatalog\ACC-PROD"
|
||||
) else (
|
||||
set "OUTPUT=%~3"
|
||||
)
|
||||
|
||||
"%EXE%" --merge --acc-json "%~1" --prod-json "%~2" --output "%OUTPUT%"
|
||||
exit /b %ERRORLEVEL%
|
||||
|
||||
:usage
|
||||
echo Aufruf:
|
||||
echo run-merge.cmd ACC-SNAPSHOT.json PROD-SNAPSHOT.json [AUSGABEORDNER]
|
||||
exit /b 2
|
||||
@@ -16,6 +16,7 @@ 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%\deployment\run-merge.cmd" "%DEPLOY%\" >nul
|
||||
copy "%ROOT%\Readme.md" "%DEPLOY%\" >nul
|
||||
copy "%ROOT%\Dokumentation.md" "%DEPLOY%\" >nul
|
||||
|
||||
|
||||
@@ -5,6 +5,5 @@
|
||||
</startup>
|
||||
<appSettings>
|
||||
<add key="WmiTimeoutSeconds" value="30" />
|
||||
<add key="MaxRowsPerArtifactType" value="10000" />
|
||||
</appSettings>
|
||||
</configuration>
|
||||
|
||||
@@ -41,18 +41,18 @@
|
||||
<Reference Include="System.IO.Compression" />
|
||||
<Reference Include="System.IO.Compression.FileSystem" />
|
||||
<Reference Include="System.Management" />
|
||||
<Reference Include="System.Runtime.Serialization" />
|
||||
<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="Persistence\SnapshotStore.cs" />
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Reporting\XlsxReportWriter.cs" />
|
||||
|
||||
@@ -3,14 +3,13 @@ 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.
|
||||
/// Liest nur die Daten, die für die Phase-1-Ansicht erforderlich sind.
|
||||
/// </summary>
|
||||
internal sealed class BizTalkWmiCollector
|
||||
{
|
||||
@@ -18,282 +17,156 @@ namespace BizTalkApplicationCatalog.Collectors
|
||||
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)
|
||||
int timeoutSeconds)
|
||||
{
|
||||
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");
|
||||
}
|
||||
CollectReceivePortMappings();
|
||||
CollectSendPortEndpoints();
|
||||
CollectReceiveLocationEndpoints();
|
||||
|
||||
document.Applications.Sort((left, right) =>
|
||||
string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase));
|
||||
document.Artifacts.Sort(CompareArtifacts);
|
||||
document.Hosts.Sort((left, right) =>
|
||||
document.Endpoints.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);
|
||||
var application = string.Compare(
|
||||
left.ApplicationName,
|
||||
right.ApplicationName,
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
return application != 0
|
||||
? application
|
||||
: string.Compare(
|
||||
left.AdapterType,
|
||||
right.AdapterType,
|
||||
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
|
||||
if (!string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
Name = name,
|
||||
Description = First(row, "Description"),
|
||||
Status = FormatStatus(First(row, "Status")),
|
||||
IsDefault = FormatBoolean(First(row, "IsDefault")),
|
||||
Source = "MSBTS_Application"
|
||||
});
|
||||
count++;
|
||||
document.Applications.Add(new ApplicationRecord { Name = name.Trim() });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
if (document.Applications.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"MSBTS_Application lieferte keine Anwendungen. Zielserver und Berechtigung prüfen.");
|
||||
"MSBTS_Application lieferte keine Anwendungen.");
|
||||
}
|
||||
|
||||
logger.Info(count + " installierte BizTalk-Anwendung(en) gefunden.");
|
||||
logger.Info(document.Applications.Count
|
||||
+ " BizTalk-Anwendung(en) gefunden.");
|
||||
}
|
||||
|
||||
private void CollectOptionalArtifacts(ArtifactDescriptor descriptor)
|
||||
{
|
||||
try
|
||||
private void CollectReceivePortMappings()
|
||||
{
|
||||
var count = 0;
|
||||
var truncated = false;
|
||||
foreach (var row in Query("SELECT * FROM " + descriptor.ClassName))
|
||||
foreach (var row in Query("SELECT * FROM MSBTS_ReceivePort"))
|
||||
{
|
||||
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");
|
||||
var name = First(row, "Name", "ReceivePortName");
|
||||
var application = First(row, "ApplicationName", "Application");
|
||||
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")
|
||||
});
|
||||
receivePortApplications[name] = application;
|
||||
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;
|
||||
}
|
||||
logger.Info(count + " Receive-Port-Zuordnung(en) gelesen.");
|
||||
}
|
||||
|
||||
private ArtifactRecord CreateArtifact(ManagementBaseObject row, string type)
|
||||
private void CollectSendPortEndpoints()
|
||||
{
|
||||
var record = new ArtifactRecord
|
||||
var count = 0;
|
||||
foreach (var row in Query("SELECT * FROM MSBTS_SendPort"))
|
||||
{
|
||||
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(
|
||||
using (row)
|
||||
{
|
||||
var application = First(row, "ApplicationName", "Application");
|
||||
AddEndpoint(application, First(
|
||||
row,
|
||||
"PTTransportType",
|
||||
"PrimaryTransportType",
|
||||
"AdapterName",
|
||||
"TransportType"),
|
||||
ParentName = First(row, "ReceivePortName", "SendPortGroupName"),
|
||||
Address = SensitiveDataSanitizer.Sanitize(First(
|
||||
row,
|
||||
"PTAddress",
|
||||
"InboundTransportURL",
|
||||
"Address"))
|
||||
};
|
||||
"TransportType"));
|
||||
count++;
|
||||
|
||||
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;
|
||||
var secondaryAdapter = First(
|
||||
row,
|
||||
"STTransportType",
|
||||
"SecondaryTransportType");
|
||||
if (!string.IsNullOrWhiteSpace(secondaryAdapter))
|
||||
{
|
||||
AddEndpoint(application, secondaryAdapter);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.Info(count + " Send-Endpunkt(e) gefunden.");
|
||||
}
|
||||
|
||||
private static IEnumerable<ArtifactDescriptor> ArtifactDescriptors()
|
||||
private void CollectReceiveLocationEndpoints()
|
||||
{
|
||||
return new[]
|
||||
var count = 0;
|
||||
foreach (var row in Query("SELECT * FROM MSBTS_ReceiveLocation"))
|
||||
{
|
||||
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")
|
||||
};
|
||||
using (row)
|
||||
{
|
||||
var application = First(row, "ApplicationName", "Application");
|
||||
if (string.IsNullOrWhiteSpace(application))
|
||||
{
|
||||
var receivePort = First(row, "ReceivePortName");
|
||||
receivePortApplications.TryGetValue(receivePort, out application);
|
||||
}
|
||||
|
||||
AddEndpoint(
|
||||
application,
|
||||
First(row, "AdapterName", "TransportType", "PTTransportType"));
|
||||
count++;
|
||||
}
|
||||
}
|
||||
logger.Info(count + " Receive-Endpunkt(e) gefunden.");
|
||||
}
|
||||
|
||||
private void AddEndpoint(string applicationName, string adapterType)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(applicationName))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Ein Endpunkt konnte keiner BizTalk-Anwendung zugeordnet werden.");
|
||||
}
|
||||
|
||||
document.Endpoints.Add(new EndpointRecord
|
||||
{
|
||||
ApplicationName = applicationName.Trim(),
|
||||
AdapterType = string.IsNullOrWhiteSpace(adapterType)
|
||||
? "Unbekannt"
|
||||
: adapterType.Trim()
|
||||
});
|
||||
}
|
||||
|
||||
private List<ManagementObject> Query(string query)
|
||||
@@ -304,38 +177,15 @@ namespace BizTalkApplicationCatalog.Collectors
|
||||
Rewindable = false,
|
||||
Timeout = timeout
|
||||
};
|
||||
using (var searcher = new ManagementObjectSearcher(scope, new ObjectQuery(query), options))
|
||||
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)
|
||||
@@ -344,78 +194,15 @@ namespace BizTalkApplicationCatalog.Collectors
|
||||
{
|
||||
var value = row[name];
|
||||
if (value == null) continue;
|
||||
var text = ConvertValue(value);
|
||||
var text = Convert.ToString(value, CultureInfo.InvariantCulture);
|
||||
if (!string.IsNullOrWhiteSpace(text)) return text;
|
||||
}
|
||||
catch (Exception exception) when (IsRecoverableWmiException(exception))
|
||||
catch (ManagementException)
|
||||
{
|
||||
// Die Eigenschaft ist in dieser BizTalk-Version/Klasse nicht vorhanden.
|
||||
// WMI-Properties unterscheiden sich zwischen Providerständen.
|
||||
}
|
||||
}
|
||||
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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,223 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,6 @@ 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 =
|
||||
@@ -14,14 +11,12 @@ namespace BizTalkApplicationCatalog.Configuration
|
||||
|
||||
public string EnvironmentName { get; private set; }
|
||||
public string OutputDirectory { get; private set; }
|
||||
public string ManagementServer { get; private set; }
|
||||
public string ManagementDatabase { get; private set; }
|
||||
public string AccSnapshotPath { get; private set; }
|
||||
public string ProdSnapshotPath { get; private set; }
|
||||
public bool MergeMode { 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();
|
||||
@@ -36,11 +31,14 @@ namespace BizTalkApplicationCatalog.Configuration
|
||||
case "--output":
|
||||
result.OutputDirectory = Value(args, ref index, argument);
|
||||
break;
|
||||
case "--management-server":
|
||||
result.ManagementServer = Value(args, ref index, argument);
|
||||
case "--merge":
|
||||
result.MergeMode = true;
|
||||
break;
|
||||
case "--management-database":
|
||||
result.ManagementDatabase = Value(args, ref index, argument);
|
||||
case "--acc-json":
|
||||
result.AccSnapshotPath = Value(args, ref index, argument);
|
||||
break;
|
||||
case "--prod-json":
|
||||
result.ProdSnapshotPath = Value(args, ref index, argument);
|
||||
break;
|
||||
case "--self-test":
|
||||
result.SelfTest = true;
|
||||
@@ -55,31 +53,54 @@ namespace BizTalkApplicationCatalog.Configuration
|
||||
}
|
||||
}
|
||||
|
||||
if (result.SelfTest || result.ShowHelp)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
if (result.SelfTest || result.ShowHelp) return result;
|
||||
|
||||
if (result.MergeMode)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(result.EnvironmentName))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"--environment darf nicht mit --merge kombiniert werden.");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(result.AccSnapshotPath)
|
||||
|| string.IsNullOrWhiteSpace(result.ProdSnapshotPath))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"--merge benötigt --acc-json und --prod-json.");
|
||||
}
|
||||
result.AccSnapshotPath = FullPath(result.AccSnapshotPath);
|
||||
result.ProdSnapshotPath = FullPath(result.ProdSnapshotPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(result.AccSnapshotPath)
|
||||
|| !string.IsNullOrWhiteSpace(result.ProdSnapshotPath))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"--acc-json und --prod-json sind nur mit --merge zulässig.");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(result.EnvironmentName))
|
||||
{
|
||||
throw new ArgumentException("--environment fehlt.");
|
||||
}
|
||||
|
||||
result.EnvironmentName = UnsafeEnvironment.Replace(
|
||||
result.EnvironmentName.Trim().ToUpperInvariant(), "-").Trim('-');
|
||||
if (result.EnvironmentName.Length == 0)
|
||||
result.EnvironmentName.Trim().ToUpperInvariant(),
|
||||
"-").Trim('-');
|
||||
if (result.EnvironmentName != "ACC" && result.EnvironmentName != "PROD")
|
||||
{
|
||||
throw new ArgumentException("--environment enthält keinen gültigen Namen.");
|
||||
throw new ArgumentException(
|
||||
"--environment muss ACC oder PROD sein.");
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(result.OutputDirectory))
|
||||
{
|
||||
result.OutputDirectory = Path.Combine(
|
||||
Environment.CurrentDirectory, "BizTalk-Anwendungsinventar", result.EnvironmentName);
|
||||
Environment.CurrentDirectory,
|
||||
"BizTalk-Anwendungskatalog",
|
||||
result.MergeMode ? "ACC-PROD" : result.EnvironmentName);
|
||||
}
|
||||
|
||||
result.OutputDirectory = Path.GetFullPath(
|
||||
Environment.ExpandEnvironmentVariables(result.OutputDirectory));
|
||||
result.OutputDirectory = FullPath(result.OutputDirectory);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -87,16 +108,22 @@ namespace BizTalkApplicationCatalog.Configuration
|
||||
{
|
||||
return string.Join(Environment.NewLine, new[]
|
||||
{
|
||||
"BEW BizTalk Application Catalog",
|
||||
"BEW BizTalk Application Catalog – Phase 1",
|
||||
string.Empty,
|
||||
"Aufruf:",
|
||||
" BizTalkApplicationCatalog.exe --environment ACC [--output PFAD]",
|
||||
" BizTalkApplicationCatalog.exe --environment PROD [--output PFAD]",
|
||||
"Lokalen Snapshot und lokale Excel-Sicht erzeugen:",
|
||||
" BizTalkApplicationCatalog.exe --environment ACC --output PFAD",
|
||||
" BizTalkApplicationCatalog.exe --environment PROD --output PFAD",
|
||||
string.Empty,
|
||||
"ACC und PROD zur gemeinsamen Excel-Sicht zusammenführen:",
|
||||
" BizTalkApplicationCatalog.exe --merge --acc-json ACC.json --prod-json PROD.json --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.",
|
||||
" --environment ACC|PROD Lokal zu inventarisierende Umgebung.",
|
||||
" --output PFAD Zielordner für XLSX, JSON und Log.",
|
||||
" --merge Führt zwei vollständige JSON-Snapshots zusammen.",
|
||||
" --acc-json PFAD Vollständiger ACC-Snapshot.",
|
||||
" --prod-json PFAD Vollständiger PROD-Snapshot.",
|
||||
" --self-test Prüft JSON, Merge und XLSX ohne BizTalk.",
|
||||
" --help Diese Hilfe."
|
||||
});
|
||||
}
|
||||
@@ -104,12 +131,17 @@ namespace BizTalkApplicationCatalog.Configuration
|
||||
private static string Value(string[] args, ref int index, string option)
|
||||
{
|
||||
index++;
|
||||
if (index >= args.Length || args[index].StartsWith("--", StringComparison.Ordinal))
|
||||
if (index >= args.Length
|
||||
|| args[index].StartsWith("--", StringComparison.Ordinal))
|
||||
{
|
||||
throw new ArgumentException("Wert für " + option + " fehlt.");
|
||||
}
|
||||
|
||||
return args[index];
|
||||
}
|
||||
|
||||
private static string FullPath(string value)
|
||||
{
|
||||
return Path.GetFullPath(Environment.ExpandEnvironmentVariables(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,22 @@
|
||||
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.Persistence;
|
||||
using BizTalkApplicationCatalog.Reporting;
|
||||
|
||||
namespace BizTalkApplicationCatalog.Infrastructure
|
||||
{
|
||||
/// <summary>
|
||||
/// Prüft die zentrale Berichtslogik ohne Zugriff auf Windows- oder BizTalk-WMI.
|
||||
/// Prüft Snapshot, Merge und Excel-Ausgabe ohne BizTalk-Zugriff.
|
||||
/// </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))
|
||||
@@ -29,11 +24,26 @@ namespace BizTalkApplicationCatalog.Infrastructure
|
||||
Path.GetTempPath(),
|
||||
"BizTalkApplicationCatalogTests-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(directory);
|
||||
var path = keepOutput ? Path.GetFullPath(outputPath) : Path.Combine(directory, "test.xlsx");
|
||||
var path = keepOutput
|
||||
? Path.GetFullPath(outputPath)
|
||||
: Path.Combine(directory, "phase1-test.xlsx");
|
||||
var jsonPath = Path.Combine(
|
||||
directory,
|
||||
"phase1-test-" + Guid.NewGuid().ToString("N") + ".json");
|
||||
|
||||
try
|
||||
{
|
||||
var document = SampleDocument();
|
||||
new XlsxReportWriter().Write(document, path);
|
||||
var acc = AccSnapshot();
|
||||
SnapshotStore.Save(acc, jsonPath);
|
||||
var loadedAcc = SnapshotStore.Load(jsonPath);
|
||||
SnapshotStore.ValidateForMerge(loadedAcc, "ACC");
|
||||
Assert(
|
||||
loadedAcc.Applications.Count == 2,
|
||||
"JSON-Roundtrip hat Anwendungen verloren.");
|
||||
|
||||
var prod = ProdSnapshot();
|
||||
SnapshotStore.ValidateForMerge(prod, "PROD");
|
||||
new XlsxReportWriter().Write(new[] { loadedAcc, prod }, path);
|
||||
Assert(File.Exists(path), "XLSX-Datei wurde nicht erzeugt.");
|
||||
|
||||
using (var archive = ZipFile.OpenRead(path))
|
||||
@@ -46,19 +56,18 @@ namespace BizTalkApplicationCatalog.Infrastructure
|
||||
"docProps/app.xml",
|
||||
"xl/workbook.xml",
|
||||
"xl/_rels/workbook.xml.rels",
|
||||
"xl/styles.xml"
|
||||
"xl/styles.xml",
|
||||
"xl/worksheets/sheet1.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);
|
||||
archive.GetEntry(name) != null,
|
||||
"XLSX-Part fehlt: " + name);
|
||||
}
|
||||
Assert(
|
||||
archive.GetEntry("xl/worksheets/sheet2.xml") == null,
|
||||
"Die Arbeitsmappe enthält mehr als ein Datenblatt.");
|
||||
|
||||
foreach (var entry in archive.Entries.Where(item =>
|
||||
item.FullName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase)
|
||||
@@ -74,42 +83,60 @@ namespace BizTalkApplicationCatalog.Infrastructure
|
||||
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.");
|
||||
.Select(item => (string)item.Attribute("name"))
|
||||
.ToList();
|
||||
Assert(
|
||||
names.SequenceEqual(new[] { "Phase 1" }),
|
||||
"Es wird nicht exakt das Blatt 'Phase 1' erzeugt.");
|
||||
|
||||
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.");
|
||||
contentTypes.Root
|
||||
.Elements(contentTypeNamespace + "Override")
|
||||
.Count() == 5,
|
||||
"Content-Type-Overrides sind unvollständig.");
|
||||
|
||||
var relationships = LoadXml(archive, "_rels/.rels");
|
||||
XNamespace relationshipNamespace =
|
||||
"http://schemas.openxmlformats.org/package/2006/relationships";
|
||||
var worksheet = LoadXml(
|
||||
archive,
|
||||
"xl/worksheets/sheet1.xml");
|
||||
Assert(
|
||||
relationships.Root.Elements(relationshipNamespace + "Relationship").Count() == 3,
|
||||
"Paketbeziehungen sind unvollständig oder im falschen Namespace.");
|
||||
worksheet.Descendants(spreadsheet + "mergeCell")
|
||||
.Select(item => (string)item.Attribute("ref"))
|
||||
.Contains("A1:G1"),
|
||||
"Titelzeile ist nicht zusammengeführt.");
|
||||
Assert(
|
||||
(string)worksheet
|
||||
.Descendants(spreadsheet + "autoFilter")
|
||||
.Single()
|
||||
.Attribute("ref") == "A2:G5",
|
||||
"Filterbereich ist falsch.");
|
||||
|
||||
var combinedText = new StringBuilder();
|
||||
foreach (var entry in archive.Entries.Where(item =>
|
||||
item.FullName.StartsWith("xl/worksheets/", StringComparison.Ordinal)))
|
||||
string text;
|
||||
using (var reader = new StreamReader(
|
||||
archive.GetEntry("xl/worksheets/sheet1.xml").Open(),
|
||||
Encoding.UTF8))
|
||||
{
|
||||
using (var reader = new StreamReader(entry.Open(), Encoding.UTF8))
|
||||
{
|
||||
combinedText.Append(reader.ReadToEnd());
|
||||
text = reader.ReadToEnd();
|
||||
}
|
||||
}
|
||||
Assert(!combinedText.ToString().Contains("supersecret"), "XLSX enthält Testkennwort.");
|
||||
Assert(combinedText.ToString().Contains("[REDACTED]"), "Redaktionsmarker fehlt.");
|
||||
Assert(text.Contains("BizTalk-Anwendung in ACC"), "ACC-Spalte fehlt.");
|
||||
Assert(text.Contains("BizTalk-Anwendung in PRD"), "PRD-Spalte fehlt.");
|
||||
Assert(text.Contains("FILE (1x) / SFTP (1x)"), "Adapteraggregation ist falsch.");
|
||||
Assert(text.Contains("Nur in ACC vorhanden"), "ACC-Hinweis fehlt.");
|
||||
Assert(text.Contains("Nur in PRD vorhanden"), "PRD-Hinweis fehlt.");
|
||||
Assert(text.Contains("Gesamt: 3 BizTalk-Anwendungen"), "Summenzeile ist falsch.");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!keepOutput && Directory.Exists(directory)) Directory.Delete(directory, true);
|
||||
if (File.Exists(jsonPath)) File.Delete(jsonPath);
|
||||
if (!keepOutput && Directory.Exists(directory))
|
||||
{
|
||||
Directory.Delete(directory, true);
|
||||
}
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
@@ -120,58 +147,56 @@ namespace BizTalkApplicationCatalog.Infrastructure
|
||||
}
|
||||
}
|
||||
|
||||
private static InventoryDocument SampleDocument()
|
||||
private static CatalogSnapshot AccSnapshot()
|
||||
{
|
||||
var document = new InventoryDocument
|
||||
var snapshot = Snapshot("ACC", "BIZTALK-ACC");
|
||||
snapshot.Applications.Add(Application(
|
||||
"OrderProcessing",
|
||||
new AdapterCount { AdapterType = "FILE", Count = 1 },
|
||||
new AdapterCount { AdapterType = "SFTP", Count = 1 }));
|
||||
snapshot.Applications.Add(Application(
|
||||
"AccOnly",
|
||||
new AdapterCount { AdapterType = "FILE", Count = 1 }));
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private static CatalogSnapshot ProdSnapshot()
|
||||
{
|
||||
EnvironmentName = "ACC",
|
||||
ComputerName = "BIZTALK-ACC",
|
||||
ToolVersion = "1.0.0.0",
|
||||
ManagementServer = "SQL-ACC",
|
||||
ManagementDatabase = "BizTalkMgmtDb",
|
||||
StartedUtc = DateTime.UtcNow.AddSeconds(-1),
|
||||
CompletedUtc = DateTime.UtcNow
|
||||
var snapshot = Snapshot("PROD", "BIZTALK-PROD");
|
||||
snapshot.Applications.Add(Application(
|
||||
"OrderProcessing",
|
||||
new AdapterCount { AdapterType = "FILE", Count = 1 },
|
||||
new AdapterCount { AdapterType = "SFTP", Count = 1 }));
|
||||
snapshot.Applications.Add(Application(
|
||||
"ProdOnly",
|
||||
new AdapterCount { AdapterType = "SFTP", Count = 1 }));
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private static CatalogSnapshot Snapshot(
|
||||
string environment,
|
||||
string computer)
|
||||
{
|
||||
return new CatalogSnapshot
|
||||
{
|
||||
EnvironmentName = environment,
|
||||
ComputerName = computer,
|
||||
CreatedUtc = DateTime.UtcNow.ToString("o"),
|
||||
IsComplete = true
|
||||
};
|
||||
document.SystemProperties.Add(new NameValueRecord(
|
||||
"Betriebssystem", "Windows Server 2019", "Self-Test"));
|
||||
document.Applications.Add(new ApplicationRecord
|
||||
}
|
||||
|
||||
private static CatalogApplication Application(
|
||||
string name,
|
||||
params AdapterCount[] adapters)
|
||||
{
|
||||
Name = "OrderProcessing",
|
||||
Description = "Aufträge & Sonderzeichen <Test>",
|
||||
Status = "Gestartet (2)",
|
||||
IsDefault = "Nein",
|
||||
Source = "Self-Test"
|
||||
});
|
||||
var port = new ArtifactRecord
|
||||
var result = new CatalogApplication
|
||||
{
|
||||
Type = "Send Port",
|
||||
ApplicationName = "OrderProcessing",
|
||||
Name = "Send_Orders",
|
||||
Status = "Gestartet (2)",
|
||||
HostName = "SendHost",
|
||||
AdapterName = "WCF-Custom",
|
||||
Address = SensitiveDataSanitizer.Sanitize(
|
||||
"https://service/orders?password=supersecret")
|
||||
Name = name,
|
||||
EndpointCount = adapters.Sum(item => item.Count)
|
||||
};
|
||||
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;
|
||||
result.AdapterCounts.AddRange(adapters);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static XDocument LoadXml(ZipArchive archive, string name)
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace BizTalkApplicationCatalog.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Enthält die vollständig normalisierten Ergebnisse eines Inventarlaufs.
|
||||
/// Arbeitsmodell eines lokalen, ausschließlich lesenden 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>();
|
||||
Endpoints = new List<EndpointRecord>();
|
||||
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<EndpointRecord> Endpoints { get; private set; }
|
||||
public List<Finding> Findings { get; private set; }
|
||||
public List<SectionStatus> SectionStatuses { get; private set; }
|
||||
|
||||
@@ -48,57 +40,12 @@ namespace BizTalkApplicationCatalog.Models
|
||||
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
|
||||
internal sealed class EndpointRecord
|
||||
{
|
||||
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; }
|
||||
public string AdapterType { get; set; }
|
||||
}
|
||||
|
||||
internal sealed class Finding
|
||||
@@ -118,21 +65,62 @@ namespace BizTalkApplicationCatalog.Models
|
||||
public long DurationMilliseconds { get; set; }
|
||||
}
|
||||
|
||||
internal sealed class NameValueRecord
|
||||
/// <summary>
|
||||
/// Kleines, transportierbares JSON-Modell für den ACC/PRD-Abgleich.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
internal sealed class CatalogSnapshot
|
||||
{
|
||||
public NameValueRecord()
|
||||
public CatalogSnapshot()
|
||||
{
|
||||
SchemaVersion = 1;
|
||||
Applications = new List<CatalogApplication>();
|
||||
}
|
||||
|
||||
public NameValueRecord(string name, string value, string source = "")
|
||||
{
|
||||
Name = name ?? string.Empty;
|
||||
Value = value ?? string.Empty;
|
||||
Source = source ?? string.Empty;
|
||||
[DataMember(Order = 1)]
|
||||
public int SchemaVersion { get; set; }
|
||||
|
||||
[DataMember(Order = 2)]
|
||||
public string EnvironmentName { get; set; }
|
||||
|
||||
[DataMember(Order = 3)]
|
||||
public string ComputerName { get; set; }
|
||||
|
||||
[DataMember(Order = 4)]
|
||||
public string CreatedUtc { get; set; }
|
||||
|
||||
[DataMember(Order = 5)]
|
||||
public bool IsComplete { get; set; }
|
||||
|
||||
[DataMember(Order = 6)]
|
||||
public List<CatalogApplication> Applications { get; set; }
|
||||
}
|
||||
|
||||
[DataContract]
|
||||
internal sealed class CatalogApplication
|
||||
{
|
||||
public CatalogApplication()
|
||||
{
|
||||
AdapterCounts = new List<AdapterCount>();
|
||||
}
|
||||
|
||||
[DataMember(Order = 1)]
|
||||
public string Name { get; set; }
|
||||
public string Value { get; set; }
|
||||
public string Source { get; set; }
|
||||
|
||||
[DataMember(Order = 2)]
|
||||
public int EndpointCount { get; set; }
|
||||
|
||||
[DataMember(Order = 3)]
|
||||
public List<AdapterCount> AdapterCounts { get; set; }
|
||||
}
|
||||
|
||||
[DataContract]
|
||||
internal sealed class AdapterCount
|
||||
{
|
||||
[DataMember(Order = 1)]
|
||||
public string AdapterType { get; set; }
|
||||
|
||||
[DataMember(Order = 2)]
|
||||
public int Count { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization.Json;
|
||||
using System.Text;
|
||||
using BizTalkApplicationCatalog.Models;
|
||||
|
||||
namespace BizTalkApplicationCatalog.Persistence
|
||||
{
|
||||
/// <summary>
|
||||
/// Erzeugt und liest die kompakten JSON-Snapshots für den ACC/PRD-Abgleich.
|
||||
/// </summary>
|
||||
internal static class SnapshotStore
|
||||
{
|
||||
public static CatalogSnapshot FromInventory(
|
||||
InventoryDocument document,
|
||||
bool isComplete)
|
||||
{
|
||||
if (document == null) throw new ArgumentNullException("document");
|
||||
|
||||
var snapshot = new CatalogSnapshot
|
||||
{
|
||||
EnvironmentName = document.EnvironmentName,
|
||||
ComputerName = document.ComputerName,
|
||||
CreatedUtc = document.CompletedUtc.ToString("o"),
|
||||
IsComplete = isComplete
|
||||
};
|
||||
|
||||
foreach (var application in document.Applications)
|
||||
{
|
||||
var endpoints = document.Endpoints.Where(item =>
|
||||
string.Equals(
|
||||
item.ApplicationName,
|
||||
application.Name,
|
||||
StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
var catalogApplication = new CatalogApplication
|
||||
{
|
||||
Name = application.Name,
|
||||
EndpointCount = endpoints.Count
|
||||
};
|
||||
|
||||
foreach (var adapter in endpoints
|
||||
.GroupBy(item => item.AdapterType, StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(item => item.Key, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
catalogApplication.AdapterCounts.Add(new AdapterCount
|
||||
{
|
||||
AdapterType = adapter.Key,
|
||||
Count = adapter.Count()
|
||||
});
|
||||
}
|
||||
snapshot.Applications.Add(catalogApplication);
|
||||
}
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
public static void Save(CatalogSnapshot snapshot, string path)
|
||||
{
|
||||
if (snapshot == null) throw new ArgumentNullException("snapshot");
|
||||
var fullPath = Path.GetFullPath(path);
|
||||
var directory = Path.GetDirectoryName(fullPath);
|
||||
if (!string.IsNullOrWhiteSpace(directory)) Directory.CreateDirectory(directory);
|
||||
var temporaryPath = fullPath + "." + Guid.NewGuid().ToString("N") + ".tmp";
|
||||
try
|
||||
{
|
||||
using (var stream = File.Create(temporaryPath))
|
||||
using (var writer = JsonReaderWriterFactory.CreateJsonWriter(
|
||||
stream,
|
||||
Encoding.UTF8,
|
||||
true,
|
||||
true))
|
||||
{
|
||||
Serializer().WriteObject(writer, snapshot);
|
||||
}
|
||||
|
||||
if (File.Exists(fullPath)) File.Delete(fullPath);
|
||||
File.Move(temporaryPath, fullPath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(temporaryPath)) File.Delete(temporaryPath);
|
||||
}
|
||||
}
|
||||
|
||||
public static CatalogSnapshot Load(string path)
|
||||
{
|
||||
var fullPath = Path.GetFullPath(path);
|
||||
using (var stream = File.OpenRead(fullPath))
|
||||
{
|
||||
var snapshot = Serializer().ReadObject(stream) as CatalogSnapshot;
|
||||
Validate(snapshot, fullPath);
|
||||
return snapshot;
|
||||
}
|
||||
}
|
||||
|
||||
public static void ValidateForMerge(
|
||||
CatalogSnapshot snapshot,
|
||||
string expectedEnvironment)
|
||||
{
|
||||
Validate(snapshot, expectedEnvironment + "-Snapshot");
|
||||
if (!snapshot.IsComplete)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
expectedEnvironment + "-Snapshot ist unvollständig und darf nicht zusammengeführt werden.");
|
||||
}
|
||||
if (!string.Equals(
|
||||
snapshot.EnvironmentName,
|
||||
expectedEnvironment,
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
"Erwartet wurde ein " + expectedEnvironment
|
||||
+ "-Snapshot, gefunden wurde "
|
||||
+ snapshot.EnvironmentName + ".");
|
||||
}
|
||||
}
|
||||
|
||||
private static DataContractJsonSerializer Serializer()
|
||||
{
|
||||
return new DataContractJsonSerializer(typeof(CatalogSnapshot));
|
||||
}
|
||||
|
||||
private static void Validate(CatalogSnapshot snapshot, string source)
|
||||
{
|
||||
if (snapshot == null)
|
||||
{
|
||||
throw new InvalidDataException("Leerer JSON-Snapshot: " + source);
|
||||
}
|
||||
if (snapshot.SchemaVersion != 1)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
"Nicht unterstützte Snapshot-Version in "
|
||||
+ source + ": " + snapshot.SchemaVersion + ".");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(snapshot.EnvironmentName))
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
"Umgebung fehlt im JSON-Snapshot: " + source);
|
||||
}
|
||||
if (snapshot.Applications == null)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
"Anwendungsliste fehlt im JSON-Snapshot: " + source);
|
||||
}
|
||||
if (snapshot.Applications.Any(item =>
|
||||
item == null
|
||||
|| string.IsNullOrWhiteSpace(item.Name)
|
||||
|| item.EndpointCount < 0
|
||||
|| item.AdapterCounts == null
|
||||
|| item.AdapterCounts.Any(adapter =>
|
||||
adapter == null
|
||||
|| string.IsNullOrWhiteSpace(adapter.AdapterType)
|
||||
|| adapter.Count < 0)
|
||||
|| item.AdapterCounts.Sum(adapter => adapter.Count) != item.EndpointCount))
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
"Ungültige Anwendungs- oder Adapterdaten im JSON-Snapshot: " + source);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,13 +7,11 @@ using BizTalkApplicationCatalog.Collectors;
|
||||
using BizTalkApplicationCatalog.Configuration;
|
||||
using BizTalkApplicationCatalog.Infrastructure;
|
||||
using BizTalkApplicationCatalog.Models;
|
||||
using BizTalkApplicationCatalog.Persistence;
|
||||
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)
|
||||
@@ -36,30 +34,32 @@ namespace BizTalkApplicationCatalog
|
||||
Console.WriteLine(CommandLineOptions.Usage());
|
||||
return 0;
|
||||
}
|
||||
if (options.SelfTest)
|
||||
{
|
||||
return RunSelfTest();
|
||||
}
|
||||
if (options.SelfTest) return RunSelfTest();
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(options.OutputDirectory);
|
||||
return options.MergeMode
|
||||
? RunMerge(options)
|
||||
: RunInventory(options);
|
||||
}
|
||||
catch (Exception exception) when (
|
||||
exception is IOException
|
||||
|| exception is UnauthorizedAccessException
|
||||
|| exception is ArgumentException)
|
||||
catch (Exception exception)
|
||||
{
|
||||
Console.Error.WriteLine("FEHLER: Ausgabeordner kann nicht erstellt werden: " + exception.Message);
|
||||
Console.Error.WriteLine("FEHLER: " + exception);
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
var timestamp = DateTime.Now.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture);
|
||||
var safeMachine = FileName(Environment.MachineName);
|
||||
var baseName = "BizTalk-Anwendungsinventar-"
|
||||
+ options.EnvironmentName + "-" + safeMachine + "-" + timestamp;
|
||||
private static int RunInventory(CommandLineOptions options)
|
||||
{
|
||||
var timestamp = Timestamp();
|
||||
var baseName = "Phase1-BizTalk-Anwendungskatalog-"
|
||||
+ options.EnvironmentName + "-"
|
||||
+ FileName(Environment.MachineName) + "-"
|
||||
+ timestamp;
|
||||
var logPath = Path.Combine(options.OutputDirectory, baseName + ".log");
|
||||
var reportPath = Path.Combine(options.OutputDirectory, baseName + ".xlsx");
|
||||
var snapshotPath = Path.Combine(options.OutputDirectory, baseName + ".json");
|
||||
|
||||
using (var logger = new ConsoleFileLogger(logPath))
|
||||
{
|
||||
@@ -70,90 +70,66 @@ namespace BizTalkApplicationCatalog
|
||||
StartedUtc = DateTime.UtcNow
|
||||
};
|
||||
|
||||
logger.Info("BEW BizTalk Application Catalog startet.");
|
||||
logger.Info("BEW BizTalk Application Catalog – Phase 1 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.");
|
||||
logger.Info("Erfasst werden nur Anwendungen, Adaptertypen und Endpunkte.");
|
||||
logger.Info("Modus: ausschließlich lesender lokaler BizTalk-WMI-Zugriff.");
|
||||
|
||||
var safe = new SafeCollector(document, logger);
|
||||
safe.Execute(
|
||||
"System und BizTalk-Gruppe",
|
||||
false,
|
||||
() => new SystemCollector(options).Collect(document));
|
||||
safe.Execute(
|
||||
"Anwendungen und Artefakte",
|
||||
new SafeCollector(document, logger).Execute(
|
||||
"Phase-1-Anwendungen und Endpunkte",
|
||||
true,
|
||||
() => new BizTalkWmiCollector(
|
||||
document,
|
||||
logger,
|
||||
ReadInt("WmiTimeoutSeconds", 30),
|
||||
ReadInt("MaxRowsPerArtifactType", 10000)).Collect());
|
||||
ReadInt("WmiTimeoutSeconds", 30)).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;
|
||||
}
|
||||
var snapshot = SnapshotStore.FromInventory(
|
||||
document,
|
||||
!document.HasRequiredFailure);
|
||||
|
||||
SnapshotStore.Save(snapshot, snapshotPath);
|
||||
logger.Info("JSON-Snapshot erzeugt: " + snapshotPath);
|
||||
new XlsxReportWriter().Write(new[] { snapshot }, reportPath);
|
||||
logger.Info("Einblättrige Excel-Sicht erzeugt: " + reportPath);
|
||||
logger.Info("Anwendungen: " + snapshot.Applications.Count);
|
||||
logger.Info("Endpunkte: "
|
||||
+ snapshot.Applications.Sum(item => item.EndpointCount));
|
||||
|
||||
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.");
|
||||
logger.Warning(
|
||||
"Snapshot ist unvollständig und wird beim Merge abgelehnt.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
logger.Info("Inventarisierung erfolgreich abgeschlossen.");
|
||||
logger.Info("Lokale Inventarisierung erfolgreich abgeschlossen.");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static void Evaluate(InventoryDocument document)
|
||||
private static int RunMerge(CommandLineOptions options)
|
||||
{
|
||||
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 baseName = "Phase1-BizTalk-Anwendungskatalog-ACC-PROD-"
|
||||
+ Timestamp();
|
||||
var logPath = Path.Combine(options.OutputDirectory, baseName + ".log");
|
||||
var reportPath = Path.Combine(options.OutputDirectory, baseName + ".xlsx");
|
||||
|
||||
var withoutApplication = document.Artifacts.Count(item =>
|
||||
string.IsNullOrWhiteSpace(item.ApplicationName));
|
||||
if (withoutApplication > 0)
|
||||
using (var logger = new ConsoleFileLogger(logPath))
|
||||
{
|
||||
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."
|
||||
});
|
||||
logger.Info("Führe ACC- und PROD-JSON-Snapshots zusammen.");
|
||||
var acc = SnapshotStore.Load(options.AccSnapshotPath);
|
||||
var prod = SnapshotStore.Load(options.ProdSnapshotPath);
|
||||
SnapshotStore.ValidateForMerge(acc, "ACC");
|
||||
SnapshotStore.ValidateForMerge(prod, "PROD");
|
||||
new XlsxReportWriter().Write(new[] { acc, prod }, reportPath);
|
||||
logger.Info("Gemeinsame Phase-1-Excel-Sicht erzeugt: " + reportPath);
|
||||
logger.Info("Anwendungen gesamt: "
|
||||
+ acc.Applications.Select(item => item.Name)
|
||||
.Concat(prod.Applications.Select(item => item.Name))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.Count());
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,7 +138,8 @@ namespace BizTalkApplicationCatalog
|
||||
try
|
||||
{
|
||||
SelfTestRunner.Run();
|
||||
Console.WriteLine("Self-Test erfolgreich: XLSX-Paket, Tabellen und Secret-Redaktion sind gültig.");
|
||||
Console.WriteLine(
|
||||
"Self-Test erfolgreich: JSON, ACC/PRD-Merge und einblättrige XLSX sind gültig.");
|
||||
return 0;
|
||||
}
|
||||
catch (Exception exception)
|
||||
@@ -184,6 +161,13 @@ namespace BizTalkApplicationCatalog
|
||||
: fallback;
|
||||
}
|
||||
|
||||
private static string Timestamp()
|
||||
{
|
||||
return DateTime.Now.ToString(
|
||||
"yyyyMMdd-HHmmss",
|
||||
CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private static string FileName(string value)
|
||||
{
|
||||
var invalid = Path.GetInvalidFileNameChars();
|
||||
|
||||
@@ -3,12 +3,12 @@ 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: AssemblyDescription("Read-only BizTalk 2020 Phase-1 application and endpoint catalog")]
|
||||
[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")]
|
||||
[assembly: AssemblyVersion("2.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("2.0.0.0")]
|
||||
|
||||
@@ -6,14 +6,12 @@ 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.
|
||||
/// Erzeugt genau ein Excel-Blatt für die Phase-1-Anwendungssicht.
|
||||
/// </summary>
|
||||
internal sealed class XlsxReportWriter
|
||||
{
|
||||
@@ -22,13 +20,19 @@ namespace BizTalkApplicationCatalog.Reporting
|
||||
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)
|
||||
public void Write(IEnumerable<CatalogSnapshot> source, string outputPath)
|
||||
{
|
||||
if (document == null) throw new ArgumentNullException("document");
|
||||
if (string.IsNullOrWhiteSpace(outputPath)) throw new ArgumentException("Ausgabepfad fehlt.");
|
||||
if (source == null) throw new ArgumentNullException("source");
|
||||
if (string.IsNullOrWhiteSpace(outputPath))
|
||||
{
|
||||
throw new ArgumentException("Ausgabepfad fehlt.");
|
||||
}
|
||||
|
||||
var snapshots = source.ToList();
|
||||
if (snapshots.Count == 0)
|
||||
{
|
||||
throw new ArgumentException("Mindestens ein Snapshot ist erforderlich.");
|
||||
}
|
||||
|
||||
var fullPath = Path.GetFullPath(outputPath);
|
||||
var directory = Path.GetDirectoryName(fullPath);
|
||||
@@ -37,20 +41,17 @@ namespace BizTalkApplicationCatalog.Reporting
|
||||
|
||||
try
|
||||
{
|
||||
var sheets = BuildSheets(document);
|
||||
var sheet = BuildPhaseOneSheet(snapshots);
|
||||
using (var archive = ZipFile.Open(temporaryPath, ZipArchiveMode.Create))
|
||||
{
|
||||
WriteContentTypes(archive, sheets.Count);
|
||||
WriteContentTypes(archive);
|
||||
WritePackageRelationships(archive);
|
||||
WriteCoreProperties(archive, document);
|
||||
WriteCoreProperties(archive, snapshots);
|
||||
WriteApplicationProperties(archive);
|
||||
WriteWorkbook(archive, sheets);
|
||||
WriteWorkbookRelationships(archive, sheets.Count);
|
||||
WriteWorkbook(archive, sheet);
|
||||
WriteWorkbookRelationships(archive);
|
||||
WriteStyles(archive);
|
||||
for (var index = 0; index < sheets.Count; index++)
|
||||
{
|
||||
WriteWorksheet(archive, index + 1, sheets[index]);
|
||||
}
|
||||
WriteWorksheet(archive, sheet);
|
||||
}
|
||||
|
||||
if (File.Exists(fullPath)) File.Delete(fullPath);
|
||||
@@ -62,296 +63,224 @@ namespace BizTalkApplicationCatalog.Reporting
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
internal static SheetDefinition BuildPhaseOneSheet(
|
||||
IList<CatalogSnapshot> snapshots)
|
||||
{
|
||||
var acc = snapshots.FirstOrDefault(item =>
|
||||
string.Equals(item.EnvironmentName, "ACC", StringComparison.OrdinalIgnoreCase));
|
||||
var prod = snapshots.FirstOrDefault(item =>
|
||||
string.Equals(item.EnvironmentName, "PROD", StringComparison.OrdinalIgnoreCase));
|
||||
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[]>
|
||||
{
|
||||
snapshots.Count > 1
|
||||
? "Phase 1 – BizTalk Anwendungen & Adapter (Quelle: ACC/PROD JSON-Snapshots)"
|
||||
: "Phase 1 – BizTalk Anwendungen & Adapter (Quelle: lokaler BizTalk-WMI-Provider)",
|
||||
"", "", "", "", "", ""
|
||||
},
|
||||
new object[]
|
||||
{
|
||||
"Kategorie", "Name", "Server", "Status", "Typ", "Windows-Gruppe",
|
||||
"Nur 32 Bit", "Vertrauenswürdig", "Adapter"
|
||||
"BizTalk-Anwendung",
|
||||
"Umgebung",
|
||||
"Adapter-Typ",
|
||||
"Anzahl Endpunkte",
|
||||
"BizTalk-Anwendung in ACC",
|
||||
"BizTalk-Anwendung in PRD",
|
||||
"Hinweis"
|
||||
}
|
||||
};
|
||||
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))
|
||||
var names = snapshots
|
||||
.SelectMany(item => item.Applications)
|
||||
.Select(item => item.Name)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(item => item, StringComparer.OrdinalIgnoreCase));
|
||||
.OrderBy(item => item, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
foreach (var name in names)
|
||||
{
|
||||
var accApplication = Find(acc, name);
|
||||
var prodApplication = Find(prod, name);
|
||||
var environments = snapshots
|
||||
.Where(snapshot => Find(snapshot, name) != null)
|
||||
.Select(snapshot => snapshot.EnvironmentName)
|
||||
.OrderBy(EnvironmentOrder)
|
||||
.ToList();
|
||||
|
||||
rows.Add(new object[]
|
||||
{
|
||||
CanonicalName(accApplication, prodApplication, name),
|
||||
EnvironmentText(accApplication, prodApplication, environments),
|
||||
AdapterText(accApplication, prodApplication, environments),
|
||||
environments.Sum(environment =>
|
||||
{
|
||||
var snapshot = snapshots.First(item =>
|
||||
string.Equals(
|
||||
item.EnvironmentName,
|
||||
environment,
|
||||
StringComparison.OrdinalIgnoreCase));
|
||||
return Find(snapshot, name).EndpointCount;
|
||||
}),
|
||||
accApplication == null ? "–" : "✓",
|
||||
prodApplication == null ? "–" : "✓",
|
||||
Hint(accApplication, prodApplication, environments)
|
||||
});
|
||||
}
|
||||
|
||||
private static string FirstNonEmpty(params string[] values)
|
||||
var accApplications = acc == null ? 0 : acc.Applications.Count;
|
||||
var prodApplications = prod == null ? 0 : prod.Applications.Count;
|
||||
var accEndpoints = acc == null ? 0 : acc.Applications.Sum(item => item.EndpointCount);
|
||||
var prodEndpoints = prod == null ? 0 : prod.Applications.Sum(item => item.EndpointCount);
|
||||
rows.Add(new object[]
|
||||
{
|
||||
return values.FirstOrDefault(item => !string.IsNullOrWhiteSpace(item)) ?? string.Empty;
|
||||
"Gesamt: " + names.Count
|
||||
+ " BizTalk-Anwendungen | ACC: " + accApplications
|
||||
+ " | PRD: " + prodApplications
|
||||
+ " | ACC Endpunkte: " + accEndpoints
|
||||
+ " | PRD Endpunkte: " + prodEndpoints,
|
||||
"", "", "", "", "", ""
|
||||
});
|
||||
|
||||
return new SheetDefinition(
|
||||
"Phase 1",
|
||||
rows,
|
||||
2,
|
||||
2,
|
||||
rows.Count - 1,
|
||||
new[] { 34d, 14d, 28d, 18d, 28d, 28d, 42d });
|
||||
}
|
||||
|
||||
private static string Empty(string value)
|
||||
private static CatalogApplication Find(
|
||||
CatalogSnapshot snapshot,
|
||||
string applicationName)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(value) ? "Nicht ermittelt" : value;
|
||||
return snapshot == null
|
||||
? null
|
||||
: snapshot.Applications.FirstOrDefault(item =>
|
||||
string.Equals(
|
||||
item.Name,
|
||||
applicationName,
|
||||
StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static void WriteWorksheet(ZipArchive archive, int sheetNumber, SheetDefinition sheet)
|
||||
private static string CanonicalName(
|
||||
CatalogApplication acc,
|
||||
CatalogApplication prod,
|
||||
string fallback)
|
||||
{
|
||||
using (var writer = CreateXmlWriter(archive, "xl/worksheets/sheet" + sheetNumber + ".xml"))
|
||||
if (acc != null) return acc.Name;
|
||||
if (prod != null) return prod.Name;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
private static string EnvironmentText(
|
||||
CatalogApplication acc,
|
||||
CatalogApplication prod,
|
||||
IList<string> environments)
|
||||
{
|
||||
if (acc != null && prod != null) return "ACC + PRD";
|
||||
if (acc != null) return "nur ACC";
|
||||
if (prod != null) return "nur PRD";
|
||||
return environments.Count == 0 ? "–" : string.Join(" + ", environments);
|
||||
}
|
||||
|
||||
private static string AdapterText(
|
||||
CatalogApplication acc,
|
||||
CatalogApplication prod,
|
||||
IList<string> environments)
|
||||
{
|
||||
var accText = AdapterCounts(acc);
|
||||
var prodText = AdapterCounts(prod);
|
||||
if (acc != null && prod != null)
|
||||
{
|
||||
return string.Equals(accText, prodText, StringComparison.OrdinalIgnoreCase)
|
||||
? accText
|
||||
: "ACC: " + accText + " | PRD: " + prodText;
|
||||
}
|
||||
if (acc != null) return accText;
|
||||
if (prod != null) return prodText;
|
||||
return environments.Count == 0 ? "–" : "Unbekannt";
|
||||
}
|
||||
|
||||
private static string AdapterCounts(CatalogApplication application)
|
||||
{
|
||||
if (application == null || application.AdapterCounts.Count == 0) return "–";
|
||||
return string.Join(
|
||||
" / ",
|
||||
application.AdapterCounts
|
||||
.OrderBy(item => item.AdapterType, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(item => item.AdapterType + " (" + item.Count + "x)"));
|
||||
}
|
||||
|
||||
private static string Hint(
|
||||
CatalogApplication acc,
|
||||
CatalogApplication prod,
|
||||
IList<string> environments)
|
||||
{
|
||||
if (acc != null && prod == null) return "Nur in ACC vorhanden";
|
||||
if (prod != null && acc == null) return "Nur in PRD vorhanden – nicht in ACC";
|
||||
if (environments.Count == 1)
|
||||
{
|
||||
return "Nur in " + environments[0] + " vorhanden";
|
||||
}
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static int EnvironmentOrder(string environment)
|
||||
{
|
||||
if (string.Equals(environment, "ACC", StringComparison.OrdinalIgnoreCase)) return 0;
|
||||
if (string.Equals(environment, "PROD", StringComparison.OrdinalIgnoreCase)) return 1;
|
||||
return 2;
|
||||
}
|
||||
|
||||
private static void WriteWorksheet(ZipArchive archive, SheetDefinition sheet)
|
||||
{
|
||||
using (var writer = CreateXmlWriter(archive, "xl/worksheets/sheet1.xml"))
|
||||
{
|
||||
writer.WriteStartDocument();
|
||||
writer.WriteStartElement("worksheet", SpreadsheetNamespace);
|
||||
writer.WriteAttributeString("xmlns", "r", null, RelationshipsNamespace);
|
||||
WriteSheetViews(writer, sheet.FreezeRows);
|
||||
WriteColumns(writer, sheet);
|
||||
WriteColumns(writer, sheet.ColumnWidths);
|
||||
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++)
|
||||
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);
|
||||
RowStyle(sheet, rowIndex));
|
||||
}
|
||||
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);
|
||||
"A" + sheet.FilterHeaderRow
|
||||
+ ":G" + (sheet.FooterRowIndex + 1 - 1));
|
||||
writer.WriteEndElement();
|
||||
|
||||
writer.WriteStartElement("mergeCells", SpreadsheetNamespace);
|
||||
writer.WriteAttributeString("count", "2");
|
||||
WriteMergedCell(writer, "A1:G1");
|
||||
WriteMergedCell(
|
||||
writer,
|
||||
"A" + (sheet.FooterRowIndex + 1)
|
||||
+ ":G" + (sheet.FooterRowIndex + 1));
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
writer.WriteStartElement("pageMargins", SpreadsheetNamespace);
|
||||
writer.WriteAttributeString("left", "0.25");
|
||||
@@ -366,54 +295,47 @@ namespace BizTalkApplicationCatalog.Reporting
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteMergedCell(XmlWriter writer, string range)
|
||||
{
|
||||
writer.WriteStartElement("mergeCell", SpreadsheetNamespace);
|
||||
writer.WriteAttributeString("ref", range);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
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;
|
||||
if (rowIndex == 1) return 1;
|
||||
if (rowIndex == sheet.FooterRowIndex) return 3;
|
||||
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(
|
||||
"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)
|
||||
private static void WriteColumns(XmlWriter writer, double[] widths)
|
||||
{
|
||||
writer.WriteStartElement("cols", SpreadsheetNamespace);
|
||||
for (var columnIndex = 0; columnIndex < sheet.MaximumColumns; columnIndex++)
|
||||
for (var index = 0; index < widths.Length; index++)
|
||||
{
|
||||
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("min", (index + 1).ToString(CultureInfo.InvariantCulture));
|
||||
writer.WriteAttributeString("max", (index + 1).ToString(CultureInfo.InvariantCulture));
|
||||
writer.WriteAttributeString("width", widths[index].ToString(CultureInfo.InvariantCulture));
|
||||
writer.WriteAttributeString("customWidth", "1");
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
@@ -429,7 +351,10 @@ namespace BizTalkApplicationCatalog.Reporting
|
||||
{
|
||||
writer.WriteStartElement("c", SpreadsheetNamespace);
|
||||
writer.WriteAttributeString("r", ColumnName(column) + row);
|
||||
if (style > 0) writer.WriteAttributeString("s", style.ToString(CultureInfo.InvariantCulture));
|
||||
if (style > 0)
|
||||
{
|
||||
writer.WriteAttributeString("s", style.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
if (IsNumber(value))
|
||||
{
|
||||
writer.WriteStartElement("v", SpreadsheetNamespace);
|
||||
@@ -442,7 +367,8 @@ namespace BizTalkApplicationCatalog.Reporting
|
||||
writer.WriteStartElement("is", SpreadsheetNamespace);
|
||||
writer.WriteStartElement("t", SpreadsheetNamespace);
|
||||
writer.WriteAttributeString("xml", "space", null, "preserve");
|
||||
writer.WriteString(ExcelText(Convert.ToString(value, CultureInfo.InvariantCulture)));
|
||||
writer.WriteString(ExcelText(
|
||||
Convert.ToString(value, CultureInfo.InvariantCulture)));
|
||||
writer.WriteEndElement();
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
@@ -479,25 +405,21 @@ namespace BizTalkApplicationCatalog.Reporting
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void WriteContentTypes(ZipArchive archive, int sheetCount)
|
||||
private static void WriteContentTypes(ZipArchive archive)
|
||||
{
|
||||
using (var writer = CreateXmlWriter(archive, "[Content_Types].xml"))
|
||||
{
|
||||
writer.WriteStartDocument();
|
||||
writer.WriteStartElement("Types", "http://schemas.openxmlformats.org/package/2006/content-types");
|
||||
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");
|
||||
}
|
||||
WriteOverride(writer, "/xl/worksheets/sheet1.xml", "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml");
|
||||
writer.WriteEndElement();
|
||||
writer.WriteEndDocument();
|
||||
}
|
||||
@@ -524,7 +446,9 @@ namespace BizTalkApplicationCatalog.Reporting
|
||||
using (var writer = CreateXmlWriter(archive, "_rels/.rels"))
|
||||
{
|
||||
writer.WriteStartDocument();
|
||||
writer.WriteStartElement("Relationships", "http://schemas.openxmlformats.org/package/2006/relationships");
|
||||
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");
|
||||
@@ -533,7 +457,9 @@ namespace BizTalkApplicationCatalog.Reporting
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteWorkbook(ZipArchive archive, List<SheetDefinition> sheets)
|
||||
private static void WriteWorkbook(
|
||||
ZipArchive archive,
|
||||
SheetDefinition sheet)
|
||||
{
|
||||
using (var writer = CreateXmlWriter(archive, "xl/workbook.xml"))
|
||||
{
|
||||
@@ -546,45 +472,39 @@ namespace BizTalkApplicationCatalog.Reporting
|
||||
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.WriteAttributeString("name", sheet.Name);
|
||||
writer.WriteAttributeString("sheetId", "1");
|
||||
writer.WriteAttributeString("r", "id", RelationshipsNamespace, "rId1");
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
writer.WriteEndElement();
|
||||
writer.WriteEndElement();
|
||||
writer.WriteEndDocument();
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteWorkbookRelationships(ZipArchive archive, int sheetCount)
|
||||
private static void WriteWorkbookRelationships(ZipArchive archive)
|
||||
{
|
||||
using (var writer = CreateXmlWriter(archive, "xl/_rels/workbook.xml.rels"))
|
||||
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.WriteStartElement(
|
||||
"Relationships",
|
||||
"http://schemas.openxmlformats.org/package/2006/relationships");
|
||||
WriteRelationship(writer, "rId1", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet", "worksheets/sheet1.xml");
|
||||
WriteRelationship(writer, "rId2", "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)
|
||||
private static void WriteRelationship(
|
||||
XmlWriter writer,
|
||||
string id,
|
||||
string type,
|
||||
string target)
|
||||
{
|
||||
writer.WriteStartElement("Relationship");
|
||||
writer.WriteAttributeString("Id", id);
|
||||
@@ -603,14 +523,14 @@ namespace BizTalkApplicationCatalog.Reporting
|
||||
writer.WriteAttributeString("count", "3");
|
||||
WriteFont(writer, false, "000000", 10);
|
||||
WriteFont(writer, true, "FFFFFF", 10);
|
||||
WriteFont(writer, true, "FFFFFF", 16);
|
||||
WriteFont(writer, true, "FFFFFF", 14);
|
||||
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");
|
||||
WritePatternFill(writer, "solid", "33475B");
|
||||
WritePatternFill(writer, "solid", "005A8B");
|
||||
writer.WriteEndElement();
|
||||
writer.WriteStartElement("borders", SpreadsheetNamespace);
|
||||
writer.WriteAttributeString("count", "2");
|
||||
@@ -619,13 +539,14 @@ namespace BizTalkApplicationCatalog.Reporting
|
||||
writer.WriteEndElement();
|
||||
writer.WriteStartElement("cellStyleXfs", SpreadsheetNamespace);
|
||||
writer.WriteAttributeString("count", "1");
|
||||
WriteXf(writer, 0, 0, 0, false);
|
||||
WriteXf(writer, 0, 0, 0);
|
||||
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.WriteAttributeString("count", "4");
|
||||
WriteXf(writer, 0, 0, 0);
|
||||
WriteXf(writer, 1, 2, 1);
|
||||
WriteXf(writer, 2, 3, 1);
|
||||
WriteXf(writer, 1, 2, 1);
|
||||
writer.WriteEndElement();
|
||||
writer.WriteStartElement("cellStyles", SpreadsheetNamespace);
|
||||
writer.WriteAttributeString("count", "1");
|
||||
@@ -640,7 +561,11 @@ namespace BizTalkApplicationCatalog.Reporting
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteFont(XmlWriter writer, bool bold, string color, int size)
|
||||
private static void WriteFont(
|
||||
XmlWriter writer,
|
||||
bool bold,
|
||||
string color,
|
||||
int size)
|
||||
{
|
||||
writer.WriteStartElement("font", SpreadsheetNamespace);
|
||||
if (bold) writer.WriteElementString("b", SpreadsheetNamespace, string.Empty);
|
||||
@@ -656,7 +581,10 @@ namespace BizTalkApplicationCatalog.Reporting
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
private static void WritePatternFill(XmlWriter writer, string pattern, string color)
|
||||
private static void WritePatternFill(
|
||||
XmlWriter writer,
|
||||
string pattern,
|
||||
string color)
|
||||
{
|
||||
writer.WriteStartElement("fill", SpreadsheetNamespace);
|
||||
writer.WriteStartElement("patternFill", SpreadsheetNamespace);
|
||||
@@ -697,8 +625,7 @@ namespace BizTalkApplicationCatalog.Reporting
|
||||
XmlWriter writer,
|
||||
int fontId,
|
||||
int fillId,
|
||||
int borderId,
|
||||
bool alignment)
|
||||
int borderId)
|
||||
{
|
||||
writer.WriteStartElement("xf", SpreadsheetNamespace);
|
||||
writer.WriteAttributeString("numFmtId", "0");
|
||||
@@ -709,32 +636,33 @@ namespace BizTalkApplicationCatalog.Reporting
|
||||
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)
|
||||
private static void WriteCoreProperties(
|
||||
ZipArchive archive,
|
||||
IList<CatalogSnapshot> snapshots)
|
||||
{
|
||||
using (var writer = CreateXmlWriter(archive, "docProps/core.xml"))
|
||||
{
|
||||
writer.WriteStartDocument();
|
||||
writer.WriteStartElement("cp", "coreProperties", "http://schemas.openxmlformats.org/package/2006/metadata/core-properties");
|
||||
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", "title", "http://purl.org/dc/elements/1.1/", "Phase 1 – BizTalk Anwendungen & Adapter");
|
||||
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.WriteString(DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture));
|
||||
writer.WriteEndElement();
|
||||
writer.WriteEndElement();
|
||||
writer.WriteEndDocument();
|
||||
@@ -746,16 +674,20 @@ namespace BizTalkApplicationCatalog.Reporting
|
||||
using (var writer = CreateXmlWriter(archive, "docProps/app.xml"))
|
||||
{
|
||||
writer.WriteStartDocument();
|
||||
writer.WriteStartElement("Properties", "http://schemas.openxmlformats.org/officeDocument/2006/extended-properties");
|
||||
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.WriteElementString("AppVersion", "2.0");
|
||||
writer.WriteEndElement();
|
||||
writer.WriteEndDocument();
|
||||
}
|
||||
}
|
||||
|
||||
private static XmlWriter CreateXmlWriter(ZipArchive archive, string path)
|
||||
private static XmlWriter CreateXmlWriter(
|
||||
ZipArchive archive,
|
||||
string path)
|
||||
{
|
||||
var entry = archive.CreateEntry(path, CompressionLevel.Optimal);
|
||||
return XmlWriter.Create(entry.Open(), new XmlWriterSettings
|
||||
@@ -771,23 +703,25 @@ namespace BizTalkApplicationCatalog.Reporting
|
||||
public SheetDefinition(
|
||||
string name,
|
||||
List<object[]> rows,
|
||||
bool autoFilter,
|
||||
int freezeRows)
|
||||
int freezeRows,
|
||||
int filterHeaderRow,
|
||||
int footerRowIndex,
|
||||
double[] columnWidths)
|
||||
{
|
||||
Name = name;
|
||||
Rows = rows;
|
||||
AutoFilter = autoFilter;
|
||||
FreezeRows = freezeRows;
|
||||
FilterHeaderRow = filterHeaderRow;
|
||||
FooterRowIndex = footerRowIndex;
|
||||
ColumnWidths = columnWidths;
|
||||
}
|
||||
|
||||
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); }
|
||||
}
|
||||
public int FilterHeaderRow { get; private set; }
|
||||
public int FooterRowIndex { get; private set; }
|
||||
public double[] ColumnWidths { get; private set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user