Reduce catalog to Phase 1 ACC/PROD view

This commit is contained in:
2026-07-27 18:03:39 +02:00
parent 2b5b97c869
commit 53fcfb84a4
17 changed files with 1208 additions and 1555 deletions
-1
View File
@@ -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"
});
document.Applications.Add(new ApplicationRecord { Name = name.Trim() });
}
}
}
if (document.Applications.Count == 0)
{
throw new InvalidOperationException(
"MSBTS_Application lieferte keine Anwendungen.");
}
logger.Info(document.Applications.Count
+ " BizTalk-Anwendung(en) gefunden.");
}
private void CollectReceivePortMappings()
{
var count = 0;
foreach (var row in Query("SELECT * FROM MSBTS_ReceivePort"))
{
using (row)
{
var name = First(row, "Name", "ReceivePortName");
var application = First(row, "ApplicationName", "Application");
if (string.IsNullOrWhiteSpace(name)) continue;
receivePortApplications[name] = application;
count++;
}
}
logger.Info(count + " Receive-Port-Zuordnung(en) gelesen.");
}
document.Coverage.Add(new CoverageRecord
private void CollectSendPortEndpoints()
{
var count = 0;
foreach (var row in Query("SELECT * FROM MSBTS_SendPort"))
{
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)
using (row)
{
var application = First(row, "ApplicationName", "Application");
AddEndpoint(application, First(
row,
"PTTransportType",
"PrimaryTransportType",
"AdapterName",
"TransportType"));
count++;
var secondaryAdapter = First(
row,
"STTransportType",
"SecondaryTransportType");
if (!string.IsNullOrWhiteSpace(secondaryAdapter))
{
AddEndpoint(application, secondaryAdapter);
count++;
}
}
}
logger.Info(count + " Send-Endpunkt(e) gefunden.");
}
private void CollectReceiveLocationEndpoints()
{
var count = 0;
foreach (var row in Query("SELECT * FROM MSBTS_ReceiveLocation"))
{
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(
"MSBTS_Application lieferte keine Anwendungen. Zielserver und Berechtigung prüfen.");
"Ein Endpunkt konnte keiner BizTalk-Anwendung zugeordnet werden.");
}
logger.Info(count + " installierte BizTalk-Anwendung(en) gefunden.");
}
private void CollectOptionalArtifacts(ArtifactDescriptor descriptor)
{
try
document.Endpoints.Add(new EndpointRecord
{
var count = 0;
var truncated = false;
foreach (var row in Query("SELECT * FROM " + descriptor.ClassName))
{
using (row)
{
if (count >= maxRowsPerType)
{
truncated = true;
break;
}
var record = CreateArtifact(row, descriptor.DisplayName);
if (string.IsNullOrWhiteSpace(record.Name)) continue;
if (record.Type == "Receive Port")
{
receivePortApplications[record.Name] = record.ApplicationName;
}
if (record.Type == "Receive Location"
&& string.IsNullOrWhiteSpace(record.ApplicationName)
&& receivePortApplications.ContainsKey(record.ParentName))
{
record.ApplicationName = receivePortApplications[record.ParentName];
}
document.Artifacts.Add(record);
count++;
}
}
document.Coverage.Add(new CoverageRecord
{
DataSource = descriptor.ClassName,
Status = truncated ? "Begrenzt" : "Vollständig",
RowCount = count,
Required = "Nein",
Message = truncated
? "Detailzeilen auf " + maxRowsPerType + " begrenzt."
: "WMI-Klasse erfolgreich gelesen."
});
logger.Info(descriptor.DisplayName + ": " + count + " Datensatz/Datensätze.");
if (truncated)
{
AddFinding(
"Warnung",
descriptor.DisplayName,
"Detailzeilen wurden bei " + maxRowsPerType + " Einträgen begrenzt.",
"MaxRowsPerArtifactType kontrolliert erhöhen und Inventar erneut ausführen.");
}
}
catch (Exception exception) when (IsRecoverableWmiException(exception))
{
document.Coverage.Add(new CoverageRecord
{
DataSource = descriptor.ClassName,
Status = "Nicht verfügbar",
RowCount = 0,
Required = "Nein",
Message = exception.Message
});
AddFinding(
"Hinweis",
descriptor.DisplayName,
"Optionale WMI-Klasse konnte nicht gelesen werden: " + exception.Message,
"Berechtigung und Verfügbarkeit der WMI-Klasse prüfen; Wert 0 nicht als fachlich bestätigt werten.");
logger.Warning(descriptor.ClassName + " nicht verfügbar: " + exception.Message);
}
}
private bool CollectOptionalHosts(string className, string category)
{
try
{
var count = 0;
foreach (var row in Query("SELECT * FROM " + className))
{
using (row)
{
var name = First(row, "Name", "HostName", "AdapterName", "RunningServer");
if (string.IsNullOrWhiteSpace(name)) continue;
document.Hosts.Add(new HostRecord
{
Category = category,
Name = name,
Server = First(row, "RunningServer", "ServerName"),
Status = FormatStatus(First(row, "ServiceState", "Status")),
Type = First(row, "HostType"),
WindowsGroup = First(row, "NTGroupName"),
Is32BitOnly = FormatBoolean(First(row, "IsHost32BitOnly")),
Trusted = FormatBoolean(First(row, "AuthTrusted")),
AdapterName = First(row, "AdapterName")
});
count++;
}
}
document.Coverage.Add(new CoverageRecord
{
DataSource = className,
Status = "Vollständig",
RowCount = count,
Required = "Nein",
Message = "WMI-Klasse erfolgreich gelesen."
});
logger.Info(category + ": " + count + " Datensatz/Datensätze.");
return true;
}
catch (Exception exception) when (IsRecoverableWmiException(exception))
{
document.Coverage.Add(new CoverageRecord
{
DataSource = className,
Status = "Nicht verfügbar",
RowCount = 0,
Required = "Nein",
Message = exception.Message
});
logger.Warning(className + " nicht verfügbar: " + exception.Message);
return false;
}
}
private ArtifactRecord CreateArtifact(ManagementBaseObject row, string type)
{
var record = new ArtifactRecord
{
Type = type,
ApplicationName = First(row, "ApplicationName", "Application"),
Name = First(
row,
"Name",
"AssemblyName",
"FullName",
"ReceivePortName",
"OrchestrationName"),
Status = FormatStatus(First(row, "Status", "ServiceStatus", "IsDisabled")),
HostName = First(row, "HostName", "SendHandler", "ReceiveHandler"),
AdapterName = First(
row,
"PTTransportType",
"AdapterName",
"TransportType"),
ParentName = First(row, "ReceivePortName", "SendPortGroupName"),
Address = SensitiveDataSanitizer.Sanitize(First(
row,
"PTAddress",
"InboundTransportURL",
"Address"))
};
AddProperty(row, record, "Description");
AddProperty(row, record, "IsTwoWay");
AddProperty(row, record, "IsDynamic");
AddProperty(row, record, "IsDisabled");
AddProperty(row, record, "ReceivePipeline");
AddProperty(row, record, "SendPipeline");
AddProperty(row, record, "STTransportType");
AddProperty(row, record, "STAddress");
AddProperty(row, record, "AssemblyName");
AddProperty(row, record, "FullName");
AddProperty(row, record, "TargetNameSpace");
AddProperty(row, record, "RootName");
AddProperty(row, record, "Tracking");
return record;
}
private static IEnumerable<ArtifactDescriptor> ArtifactDescriptors()
{
return new[]
{
new ArtifactDescriptor("MSBTS_Orchestration", "Orchestrierung"),
new ArtifactDescriptor("MSBTS_SendPort", "Send Port"),
new ArtifactDescriptor("MSBTS_SendPortGroup", "Send Port Group"),
new ArtifactDescriptor("MSBTS_ReceivePort", "Receive Port"),
new ArtifactDescriptor("MSBTS_ReceiveLocation", "Receive Location"),
new ArtifactDescriptor("MSBTS_Assembly", "Assembly"),
new ArtifactDescriptor("MSBTS_Schema", "Schema"),
new ArtifactDescriptor("MSBTS_Map", "Map"),
new ArtifactDescriptor("MSBTS_Pipeline", "Pipeline")
};
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 (string.IsNullOrWhiteSpace(result.EnvironmentName))
if (result.MergeMode)
{
throw new ArgumentException("--environment fehlt.");
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);
}
result.EnvironmentName = UnsafeEnvironment.Replace(
result.EnvironmentName.Trim().ToUpperInvariant(), "-").Trim('-');
if (result.EnvironmentName.Length == 0)
else
{
throw new ArgumentException("--environment enthält keinen gültigen Namen.");
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 != "ACC" && result.EnvironmentName != "PROD")
{
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,29 +108,40 @@ 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.",
" --help Diese Hilfe."
" --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."
});
}
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()
{
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 = "ACC",
ComputerName = "BIZTALK-ACC",
ToolVersion = "1.0.0.0",
ManagementServer = "SQL-ACC",
ManagementDatabase = "BizTalkMgmtDb",
StartedUtc = DateTime.UtcNow.AddSeconds(-1),
CompletedUtc = DateTime.UtcNow
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)
{
var result = new CatalogApplication
{
Name = "OrderProcessing",
Description = "Aufträge & Sonderzeichen <Test>",
Status = "Gestartet (2)",
IsDefault = "Nein",
Source = "Self-Test"
});
var port = new ArtifactRecord
{
Type = "Send Port",
ApplicationName = "OrderProcessing",
Name = "Send_Orders",
Status = "Gestartet (2)",
HostName = "SendHost",
AdapterName = "WCF-Custom",
Address = SensitiveDataSanitizer.Sanitize(
"https://service/orders?password=supersecret")
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 = "")
[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()
{
Name = name ?? string.Empty;
Value = value ?? string.Empty;
Source = source ?? string.Empty;
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);
}
}
}
}
+64 -80
View File
@@ -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)
var names = snapshots
.SelectMany(item => item.Applications)
.Select(item => item.Name)
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(item => item, StringComparer.OrdinalIgnoreCase)
.ToList();
return incomplete.Count == 0
? "Vollständig"
: "Teilweise siehe Abdeckung: " + string.Join(", ", incomplete);
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)
});
}
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[]
{
"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 int Count(IEnumerable<ArtifactRecord> artifacts, string type)
private static CatalogApplication Find(
CatalogSnapshot snapshot,
string applicationName)
{
return artifacts.Count(item => item.Type == type);
return snapshot == null
? null
: snapshot.Applications.FirstOrDefault(item =>
string.Equals(
item.Name,
applicationName,
StringComparison.OrdinalIgnoreCase));
}
private static string JoinDistinct(IEnumerable<string> values)
private static string CanonicalName(
CatalogApplication acc,
CatalogApplication prod,
string fallback)
{
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(
", ",
values.Where(item => !string.IsNullOrWhiteSpace(item))
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(item => item, StringComparer.OrdinalIgnoreCase));
" / ",
application.AdapterCounts
.OrderBy(item => item.AdapterType, StringComparer.OrdinalIgnoreCase)
.Select(item => item.AdapterType + " (" + item.Count + "x)"));
}
private static string FirstNonEmpty(params string[] values)
private static string Hint(
CatalogApplication acc,
CatalogApplication prod,
IList<string> environments)
{
return values.FirstOrDefault(item => !string.IsNullOrWhiteSpace(item)) ?? string.Empty;
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 string Empty(string value)
private static int EnvironmentOrder(string environment)
{
return string.IsNullOrWhiteSpace(value) ? "Nicht ermittelt" : value;
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, int sheetNumber, SheetDefinition sheet)
private static void WriteWorksheet(ZipArchive archive, SheetDefinition sheet)
{
using (var writer = CreateXmlWriter(archive, "xl/worksheets/sheet" + sheetNumber + ".xml"))
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);
writer.WriteEndElement();
}
writer.WriteStartElement("autoFilter", SpreadsheetNamespace);
writer.WriteAttributeString(
"ref",
"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,18 +295,19 @@ 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;
return 0;
}
return rowIndex == 0 ? 1 : 0;
if (rowIndex == 0) return 2;
if (rowIndex == 1) return 1;
if (rowIndex == sheet.FooterRowIndex) return 3;
return 0;
}
private static void WriteSheetViews(XmlWriter writer, int freezeRows)
@@ -385,35 +315,27 @@ namespace BizTalkApplicationCatalog.Reporting
writer.WriteStartElement("sheetViews", SpreadsheetNamespace);
writer.WriteStartElement("sheetView", SpreadsheetNamespace);
writer.WriteAttributeString("workbookViewId", "0");
if (freezeRows > 0)
{
writer.WriteStartElement("pane", SpreadsheetNamespace);
writer.WriteAttributeString("ySplit", freezeRows.ToString(CultureInfo.InvariantCulture));
writer.WriteAttributeString("topLeftCell", "A" + (freezeRows + 1));
writer.WriteAttributeString("activePane", "bottomLeft");
writer.WriteAttributeString("state", "frozen");
writer.WriteEndElement();
}
writer.WriteStartElement("pane", SpreadsheetNamespace);
writer.WriteAttributeString(
"ySplit",
freezeRows.ToString(CultureInfo.InvariantCulture));
writer.WriteAttributeString("topLeftCell", "A" + (freezeRows + 1));
writer.WriteAttributeString("activePane", "bottomLeft");
writer.WriteAttributeString("state", "frozen");
writer.WriteEndElement();
writer.WriteEndElement();
writer.WriteEndElement();
}
private static void WriteColumns(XmlWriter writer, SheetDefinition sheet)
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.WriteEndElement();
}
writer.WriteStartElement("sheet", SpreadsheetNamespace);
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.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; }
}
}
}