Initial commit: BizTalk application catalog
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<startup useLegacyV2RuntimeActivationPolicy="true">
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
|
||||
</startup>
|
||||
<appSettings>
|
||||
<add key="WmiTimeoutSeconds" value="30" />
|
||||
<add key="MaxRowsPerArtifactType" value="10000" />
|
||||
</appSettings>
|
||||
</configuration>
|
||||
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{41CB5701-3FBC-49F4-856A-6AE930B8513D}</ProjectGuid>
|
||||
<OutputType>Exe</OutputType>
|
||||
<RootNamespace>BizTalkApplicationCatalog</RootNamespace>
|
||||
<AssemblyName>BizTalkApplicationCatalog</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
|
||||
<TargetFrameworkProfile />
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<LangVersion>7.3</LangVersion>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
|
||||
<Deterministic>true</Deterministic>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<Prefer32Bit>false</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<Prefer32Bit>false</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Configuration" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.IO.Compression" />
|
||||
<Reference Include="System.IO.Compression.FileSystem" />
|
||||
<Reference Include="System.Management" />
|
||||
<Reference Include="System.Xml" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Collectors\BizTalkWmiCollector.cs" />
|
||||
<Compile Include="Collectors\SystemCollector.cs" />
|
||||
<Compile Include="Configuration\CommandLineOptions.cs" />
|
||||
<Compile Include="Infrastructure\ConsoleFileLogger.cs" />
|
||||
<Compile Include="Infrastructure\SafeCollector.cs" />
|
||||
<Compile Include="Infrastructure\SelfTestRunner.cs" />
|
||||
<Compile Include="Infrastructure\SensitiveDataSanitizer.cs" />
|
||||
<Compile Include="Models\InventoryModels.cs" />
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Reporting\XlsxReportWriter.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="App.config" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
@@ -0,0 +1,421 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Management;
|
||||
using System.Runtime.InteropServices;
|
||||
using BizTalkApplicationCatalog.Infrastructure;
|
||||
using BizTalkApplicationCatalog.Models;
|
||||
|
||||
namespace BizTalkApplicationCatalog.Collectors
|
||||
{
|
||||
/// <summary>
|
||||
/// Liest die installierten Anwendungen und zugehörigen Artefakte über den lokalen BizTalk-WMI-Provider.
|
||||
/// </summary>
|
||||
internal sealed class BizTalkWmiCollector
|
||||
{
|
||||
private readonly InventoryDocument document;
|
||||
private readonly ConsoleFileLogger logger;
|
||||
private readonly ManagementScope scope;
|
||||
private readonly TimeSpan timeout;
|
||||
private readonly int maxRowsPerType;
|
||||
private readonly Dictionary<string, string> receivePortApplications =
|
||||
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public BizTalkWmiCollector(
|
||||
InventoryDocument document,
|
||||
ConsoleFileLogger logger,
|
||||
int timeoutSeconds,
|
||||
int maxRowsPerType)
|
||||
{
|
||||
this.document = document;
|
||||
this.logger = logger;
|
||||
timeout = TimeSpan.FromSeconds(Math.Max(5, timeoutSeconds));
|
||||
this.maxRowsPerType = Math.Max(100, maxRowsPerType);
|
||||
scope = new ManagementScope(
|
||||
@"\\" + Environment.MachineName + @"\root\MicrosoftBizTalkServer");
|
||||
scope.Options.Timeout = timeout;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stellt zuerst die vollständige Primärliste her und ergänzt anschließend optionale Details.
|
||||
/// </summary>
|
||||
public void Collect()
|
||||
{
|
||||
scope.Connect();
|
||||
CollectApplications();
|
||||
|
||||
// Receive Ports werden vor Receive Locations gelesen, damit deren Anwendung
|
||||
// auch dann aufgelöst werden kann, wenn die Location sie nicht direkt liefert.
|
||||
foreach (var descriptor in ArtifactDescriptors())
|
||||
{
|
||||
CollectOptionalArtifacts(descriptor);
|
||||
}
|
||||
|
||||
CollectOptionalHosts("MSBTS_HostSetting", "Host");
|
||||
CollectOptionalHosts("MSBTS_HostInstance", "Hostinstanz");
|
||||
CollectOptionalHosts("MSBTS_ReceiveHandler", "Receive Handler");
|
||||
if (!CollectOptionalHosts("MSBTS_SendHandler2", "Send Handler"))
|
||||
{
|
||||
CollectOptionalHosts("MSBTS_SendHandler", "Send Handler");
|
||||
}
|
||||
|
||||
document.Applications.Sort((left, right) =>
|
||||
string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase));
|
||||
document.Artifacts.Sort(CompareArtifacts);
|
||||
document.Hosts.Sort((left, right) =>
|
||||
{
|
||||
var category = string.Compare(left.Category, right.Category, StringComparison.OrdinalIgnoreCase);
|
||||
return category != 0
|
||||
? category
|
||||
: string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase);
|
||||
});
|
||||
}
|
||||
|
||||
private void CollectApplications()
|
||||
{
|
||||
var count = 0;
|
||||
foreach (var row in Query("SELECT * FROM MSBTS_Application"))
|
||||
{
|
||||
using (row)
|
||||
{
|
||||
var name = First(row, "Name", "ApplicationName");
|
||||
if (string.IsNullOrWhiteSpace(name)) continue;
|
||||
document.Applications.Add(new ApplicationRecord
|
||||
{
|
||||
Name = name,
|
||||
Description = First(row, "Description"),
|
||||
Status = FormatStatus(First(row, "Status")),
|
||||
IsDefault = FormatBoolean(First(row, "IsDefault")),
|
||||
Source = "MSBTS_Application"
|
||||
});
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
document.Coverage.Add(new CoverageRecord
|
||||
{
|
||||
DataSource = "MSBTS_Application",
|
||||
Status = count > 0 ? "Vollständig" : "Fehler",
|
||||
RowCount = count,
|
||||
Required = "Ja",
|
||||
Message = count > 0
|
||||
? "Primärquelle der vollständigen Anwendungsliste."
|
||||
: "Keine Anwendung geliefert."
|
||||
});
|
||||
if (count == 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"MSBTS_Application lieferte keine Anwendungen. Zielserver und Berechtigung prüfen.");
|
||||
}
|
||||
|
||||
logger.Info(count + " installierte BizTalk-Anwendung(en) gefunden.");
|
||||
}
|
||||
|
||||
private void CollectOptionalArtifacts(ArtifactDescriptor descriptor)
|
||||
{
|
||||
try
|
||||
{
|
||||
var count = 0;
|
||||
var truncated = false;
|
||||
foreach (var row in Query("SELECT * FROM " + descriptor.ClassName))
|
||||
{
|
||||
using (row)
|
||||
{
|
||||
if (count >= maxRowsPerType)
|
||||
{
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
|
||||
var record = CreateArtifact(row, descriptor.DisplayName);
|
||||
if (string.IsNullOrWhiteSpace(record.Name)) continue;
|
||||
if (record.Type == "Receive Port")
|
||||
{
|
||||
receivePortApplications[record.Name] = record.ApplicationName;
|
||||
}
|
||||
if (record.Type == "Receive Location"
|
||||
&& string.IsNullOrWhiteSpace(record.ApplicationName)
|
||||
&& receivePortApplications.ContainsKey(record.ParentName))
|
||||
{
|
||||
record.ApplicationName = receivePortApplications[record.ParentName];
|
||||
}
|
||||
document.Artifacts.Add(record);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
document.Coverage.Add(new CoverageRecord
|
||||
{
|
||||
DataSource = descriptor.ClassName,
|
||||
Status = truncated ? "Begrenzt" : "Vollständig",
|
||||
RowCount = count,
|
||||
Required = "Nein",
|
||||
Message = truncated
|
||||
? "Detailzeilen auf " + maxRowsPerType + " begrenzt."
|
||||
: "WMI-Klasse erfolgreich gelesen."
|
||||
});
|
||||
logger.Info(descriptor.DisplayName + ": " + count + " Datensatz/Datensätze.");
|
||||
if (truncated)
|
||||
{
|
||||
AddFinding(
|
||||
"Warnung",
|
||||
descriptor.DisplayName,
|
||||
"Detailzeilen wurden bei " + maxRowsPerType + " Einträgen begrenzt.",
|
||||
"MaxRowsPerArtifactType kontrolliert erhöhen und Inventar erneut ausführen.");
|
||||
}
|
||||
}
|
||||
catch (Exception exception) when (IsRecoverableWmiException(exception))
|
||||
{
|
||||
document.Coverage.Add(new CoverageRecord
|
||||
{
|
||||
DataSource = descriptor.ClassName,
|
||||
Status = "Nicht verfügbar",
|
||||
RowCount = 0,
|
||||
Required = "Nein",
|
||||
Message = exception.Message
|
||||
});
|
||||
AddFinding(
|
||||
"Hinweis",
|
||||
descriptor.DisplayName,
|
||||
"Optionale WMI-Klasse konnte nicht gelesen werden: " + exception.Message,
|
||||
"Berechtigung und Verfügbarkeit der WMI-Klasse prüfen; Wert 0 nicht als fachlich bestätigt werten.");
|
||||
logger.Warning(descriptor.ClassName + " nicht verfügbar: " + exception.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private bool CollectOptionalHosts(string className, string category)
|
||||
{
|
||||
try
|
||||
{
|
||||
var count = 0;
|
||||
foreach (var row in Query("SELECT * FROM " + className))
|
||||
{
|
||||
using (row)
|
||||
{
|
||||
var name = First(row, "Name", "HostName", "AdapterName", "RunningServer");
|
||||
if (string.IsNullOrWhiteSpace(name)) continue;
|
||||
document.Hosts.Add(new HostRecord
|
||||
{
|
||||
Category = category,
|
||||
Name = name,
|
||||
Server = First(row, "RunningServer", "ServerName"),
|
||||
Status = FormatStatus(First(row, "ServiceState", "Status")),
|
||||
Type = First(row, "HostType"),
|
||||
WindowsGroup = First(row, "NTGroupName"),
|
||||
Is32BitOnly = FormatBoolean(First(row, "IsHost32BitOnly")),
|
||||
Trusted = FormatBoolean(First(row, "AuthTrusted")),
|
||||
AdapterName = First(row, "AdapterName")
|
||||
});
|
||||
count++;
|
||||
}
|
||||
}
|
||||
document.Coverage.Add(new CoverageRecord
|
||||
{
|
||||
DataSource = className,
|
||||
Status = "Vollständig",
|
||||
RowCount = count,
|
||||
Required = "Nein",
|
||||
Message = "WMI-Klasse erfolgreich gelesen."
|
||||
});
|
||||
logger.Info(category + ": " + count + " Datensatz/Datensätze.");
|
||||
return true;
|
||||
}
|
||||
catch (Exception exception) when (IsRecoverableWmiException(exception))
|
||||
{
|
||||
document.Coverage.Add(new CoverageRecord
|
||||
{
|
||||
DataSource = className,
|
||||
Status = "Nicht verfügbar",
|
||||
RowCount = 0,
|
||||
Required = "Nein",
|
||||
Message = exception.Message
|
||||
});
|
||||
logger.Warning(className + " nicht verfügbar: " + exception.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private ArtifactRecord CreateArtifact(ManagementBaseObject row, string type)
|
||||
{
|
||||
var record = new ArtifactRecord
|
||||
{
|
||||
Type = type,
|
||||
ApplicationName = First(row, "ApplicationName", "Application"),
|
||||
Name = First(
|
||||
row,
|
||||
"Name",
|
||||
"AssemblyName",
|
||||
"FullName",
|
||||
"ReceivePortName",
|
||||
"OrchestrationName"),
|
||||
Status = FormatStatus(First(row, "Status", "ServiceStatus", "IsDisabled")),
|
||||
HostName = First(row, "HostName", "SendHandler", "ReceiveHandler"),
|
||||
AdapterName = First(
|
||||
row,
|
||||
"PTTransportType",
|
||||
"AdapterName",
|
||||
"TransportType"),
|
||||
ParentName = First(row, "ReceivePortName", "SendPortGroupName"),
|
||||
Address = SensitiveDataSanitizer.Sanitize(First(
|
||||
row,
|
||||
"PTAddress",
|
||||
"InboundTransportURL",
|
||||
"Address"))
|
||||
};
|
||||
|
||||
AddProperty(row, record, "Description");
|
||||
AddProperty(row, record, "IsTwoWay");
|
||||
AddProperty(row, record, "IsDynamic");
|
||||
AddProperty(row, record, "IsDisabled");
|
||||
AddProperty(row, record, "ReceivePipeline");
|
||||
AddProperty(row, record, "SendPipeline");
|
||||
AddProperty(row, record, "STTransportType");
|
||||
AddProperty(row, record, "STAddress");
|
||||
AddProperty(row, record, "AssemblyName");
|
||||
AddProperty(row, record, "FullName");
|
||||
AddProperty(row, record, "TargetNameSpace");
|
||||
AddProperty(row, record, "RootName");
|
||||
AddProperty(row, record, "Tracking");
|
||||
return record;
|
||||
}
|
||||
|
||||
private static IEnumerable<ArtifactDescriptor> ArtifactDescriptors()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
new ArtifactDescriptor("MSBTS_Orchestration", "Orchestrierung"),
|
||||
new ArtifactDescriptor("MSBTS_SendPort", "Send Port"),
|
||||
new ArtifactDescriptor("MSBTS_SendPortGroup", "Send Port Group"),
|
||||
new ArtifactDescriptor("MSBTS_ReceivePort", "Receive Port"),
|
||||
new ArtifactDescriptor("MSBTS_ReceiveLocation", "Receive Location"),
|
||||
new ArtifactDescriptor("MSBTS_Assembly", "Assembly"),
|
||||
new ArtifactDescriptor("MSBTS_Schema", "Schema"),
|
||||
new ArtifactDescriptor("MSBTS_Map", "Map"),
|
||||
new ArtifactDescriptor("MSBTS_Pipeline", "Pipeline")
|
||||
};
|
||||
}
|
||||
|
||||
private List<ManagementObject> Query(string query)
|
||||
{
|
||||
var options = new EnumerationOptions
|
||||
{
|
||||
ReturnImmediately = false,
|
||||
Rewindable = false,
|
||||
Timeout = timeout
|
||||
};
|
||||
using (var searcher = new ManagementObjectSearcher(scope, new ObjectQuery(query), options))
|
||||
{
|
||||
return searcher.Get().Cast<ManagementObject>().ToList();
|
||||
}
|
||||
}
|
||||
|
||||
private void AddFinding(string severity, string area, string message, string action)
|
||||
{
|
||||
document.Findings.Add(new Finding
|
||||
{
|
||||
Severity = severity,
|
||||
Area = area,
|
||||
Message = message,
|
||||
RecommendedAction = action
|
||||
});
|
||||
}
|
||||
|
||||
private static void AddProperty(
|
||||
ManagementBaseObject row,
|
||||
ArtifactRecord target,
|
||||
string propertyName)
|
||||
{
|
||||
var value = First(row, propertyName);
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
target.Properties.Add(new NameValueRecord(
|
||||
propertyName,
|
||||
SensitiveDataSanitizer.RedactProperty(propertyName, value),
|
||||
"WMI"));
|
||||
}
|
||||
}
|
||||
|
||||
private static string First(ManagementBaseObject row, params string[] names)
|
||||
{
|
||||
foreach (var name in names)
|
||||
{
|
||||
try
|
||||
{
|
||||
var value = row[name];
|
||||
if (value == null) continue;
|
||||
var text = ConvertValue(value);
|
||||
if (!string.IsNullOrWhiteSpace(text)) return text;
|
||||
}
|
||||
catch (Exception exception) when (IsRecoverableWmiException(exception))
|
||||
{
|
||||
// Die Eigenschaft ist in dieser BizTalk-Version/Klasse nicht vorhanden.
|
||||
}
|
||||
}
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static string ConvertValue(object value)
|
||||
{
|
||||
var array = value as Array;
|
||||
if (array != null && !(value is byte[]))
|
||||
{
|
||||
return string.Join(", ", array.Cast<object>().Select(item =>
|
||||
Convert.ToString(item, CultureInfo.InvariantCulture)));
|
||||
}
|
||||
return Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty;
|
||||
}
|
||||
|
||||
private static bool IsRecoverableWmiException(Exception exception)
|
||||
{
|
||||
return exception is ManagementException
|
||||
|| exception is COMException
|
||||
|| exception is UnauthorizedAccessException;
|
||||
}
|
||||
|
||||
private static string FormatStatus(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value)) return "Unbekannt";
|
||||
switch (value.Trim())
|
||||
{
|
||||
case "1": return "Gestoppt (1)";
|
||||
case "2": return "Gestartet (2)";
|
||||
case "3": return "Teilweise gestartet (3)";
|
||||
case "True": return "Ja";
|
||||
case "False": return "Nein";
|
||||
default: return value;
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatBoolean(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value)) return "Unbekannt";
|
||||
if (value.Equals("True", StringComparison.OrdinalIgnoreCase) || value == "1") return "Ja";
|
||||
if (value.Equals("False", StringComparison.OrdinalIgnoreCase) || value == "0") return "Nein";
|
||||
return value;
|
||||
}
|
||||
|
||||
private static int CompareArtifacts(ArtifactRecord left, ArtifactRecord right)
|
||||
{
|
||||
var application = string.Compare(
|
||||
left.ApplicationName, right.ApplicationName, StringComparison.OrdinalIgnoreCase);
|
||||
if (application != 0) return application;
|
||||
var type = string.Compare(left.Type, right.Type, StringComparison.OrdinalIgnoreCase);
|
||||
return type != 0
|
||||
? type
|
||||
: string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private sealed class ArtifactDescriptor
|
||||
{
|
||||
public ArtifactDescriptor(string className, string displayName)
|
||||
{
|
||||
ClassName = className;
|
||||
DisplayName = displayName;
|
||||
}
|
||||
|
||||
public string ClassName { get; private set; }
|
||||
public string DisplayName { get; private set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Management;
|
||||
using System.Reflection;
|
||||
using Microsoft.Win32;
|
||||
using BizTalkApplicationCatalog.Configuration;
|
||||
using BizTalkApplicationCatalog.Models;
|
||||
|
||||
namespace BizTalkApplicationCatalog.Collectors
|
||||
{
|
||||
/// <summary>
|
||||
/// Erfasst lokale Windows-, BizTalk- und Gruppenmetadaten ausschließlich lesend.
|
||||
/// </summary>
|
||||
internal sealed class SystemCollector
|
||||
{
|
||||
private const string ProductKey = @"SOFTWARE\Microsoft\BizTalk Server\3.0";
|
||||
private const string AdministrationKey = ProductKey + @"\Administration";
|
||||
private readonly CommandLineOptions options;
|
||||
|
||||
public SystemCollector(CommandLineOptions options)
|
||||
{
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
public void Collect(InventoryDocument document)
|
||||
{
|
||||
document.ComputerName = Environment.MachineName;
|
||||
document.ToolVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString();
|
||||
Add(document, "Ausführender Benutzer", Environment.UserDomainName + "\\" + Environment.UserName, "Prozess");
|
||||
Add(document, "64-Bit-Betriebssystem", Environment.Is64BitOperatingSystem ? "Ja" : "Nein", "Prozess");
|
||||
Add(document, "64-Bit-Prozess", Environment.Is64BitProcess ? "Ja" : "Nein", "Prozess");
|
||||
Add(document, ".NET Runtime", Environment.Version.ToString(), "Prozess");
|
||||
|
||||
CollectOperatingSystem(document);
|
||||
CollectRegistry(document);
|
||||
CollectGroupSetting(document);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(options.ManagementServer))
|
||||
{
|
||||
document.ManagementServer = options.ManagementServer;
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(options.ManagementDatabase))
|
||||
{
|
||||
document.ManagementDatabase = options.ManagementDatabase;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(document.ManagementDatabase))
|
||||
{
|
||||
document.ManagementDatabase = "BizTalkMgmtDb";
|
||||
}
|
||||
|
||||
Add(document, "BizTalk Management SQL Server", Unknown(document.ManagementServer), "Ermittelt/Parameter");
|
||||
Add(document, "BizTalk Management Database", document.ManagementDatabase, "Ermittelt/Parameter");
|
||||
|
||||
if (document.EnvironmentName != "ACC" && document.EnvironmentName != "PROD")
|
||||
{
|
||||
document.Findings.Add(new Finding
|
||||
{
|
||||
Severity = "Warnung",
|
||||
Area = "Aufruf",
|
||||
Message = "Die Umgebung ist weder ACC noch PROD: " + document.EnvironmentName,
|
||||
RecommendedAction = "Umgebung und Zielserver vor der Ablage bestätigen."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void CollectOperatingSystem(InventoryDocument document)
|
||||
{
|
||||
using (var searcher = new ManagementObjectSearcher(
|
||||
"root\\cimv2",
|
||||
"SELECT Caption,Version,BuildNumber,OSArchitecture,LastBootUpTime FROM Win32_OperatingSystem"))
|
||||
{
|
||||
foreach (ManagementObject row in searcher.Get())
|
||||
{
|
||||
using (row)
|
||||
{
|
||||
Add(document, "Betriebssystem", Value(row, "Caption"), "Win32_OperatingSystem");
|
||||
Add(document, "Windows-Version", Value(row, "Version"), "Win32_OperatingSystem");
|
||||
Add(document, "Windows-Build", Value(row, "BuildNumber"), "Win32_OperatingSystem");
|
||||
Add(document, "Architektur", Value(row, "OSArchitecture"), "Win32_OperatingSystem");
|
||||
Add(document, "Letzter Systemstart", WmiDate(Value(row, "LastBootUpTime")), "Win32_OperatingSystem");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void CollectRegistry(InventoryDocument document)
|
||||
{
|
||||
foreach (var view in new[] { RegistryView.Registry64, RegistryView.Registry32 })
|
||||
{
|
||||
using (var baseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, view))
|
||||
using (var product = baseKey.OpenSubKey(ProductKey, false))
|
||||
{
|
||||
if (product != null)
|
||||
{
|
||||
AddRegistry(document, product, "ProductName", "BizTalk Produktname", view);
|
||||
AddRegistry(document, product, "ProductVersion", "BizTalk Produktversion", view);
|
||||
AddRegistry(document, product, "Edition", "BizTalk Edition", view);
|
||||
}
|
||||
}
|
||||
|
||||
using (var baseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, view))
|
||||
using (var administration = baseKey.OpenSubKey(AdministrationKey, false))
|
||||
{
|
||||
if (administration == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(document.ManagementServer))
|
||||
{
|
||||
document.ManagementServer = FirstRegistry(
|
||||
administration, "MgmtDBServer", "ManagementDBServer");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(document.ManagementDatabase))
|
||||
{
|
||||
document.ManagementDatabase = FirstRegistry(
|
||||
administration, "MgmtDBName", "ManagementDBName");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void CollectGroupSetting(InventoryDocument document)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var searcher = new ManagementObjectSearcher(
|
||||
@"root\MicrosoftBizTalkServer", "SELECT * FROM MSBTS_GroupSetting"))
|
||||
{
|
||||
foreach (ManagementObject row in searcher.Get())
|
||||
{
|
||||
using (row)
|
||||
{
|
||||
var server = Value(row, "MgmtDbServerName");
|
||||
var database = Value(row, "MgmtDbName");
|
||||
if (!string.IsNullOrWhiteSpace(server)) document.ManagementServer = server;
|
||||
if (!string.IsNullOrWhiteSpace(database)) document.ManagementDatabase = database;
|
||||
Add(document, "BizTalk Gruppenname", Value(row, "Name"), "MSBTS_GroupSetting");
|
||||
Add(document, "BizTalk Administratorengruppe", Value(row, "BizTalkAdministratorGroup"), "MSBTS_GroupSetting");
|
||||
Add(document, "BizTalk Operatorengruppe", Value(row, "BizTalkOperatorGroup"), "MSBTS_GroupSetting");
|
||||
Add(document, "Enterprise SSO Server", Value(row, "SSOServerName"), "MSBTS_GroupSetting");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (ManagementException exception)
|
||||
{
|
||||
document.Findings.Add(new Finding
|
||||
{
|
||||
Severity = "Hinweis",
|
||||
Area = "MSBTS_GroupSetting",
|
||||
Message = "Gruppenmetadaten konnten nicht vollständig gelesen werden: " + exception.Message,
|
||||
RecommendedAction = "WMI-Berechtigung prüfen; die Anwendungserfassung läuft unabhängig weiter."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddRegistry(
|
||||
InventoryDocument document,
|
||||
RegistryKey key,
|
||||
string valueName,
|
||||
string displayName,
|
||||
RegistryView view)
|
||||
{
|
||||
var value = Convert.ToString(key.GetValue(valueName), CultureInfo.InvariantCulture);
|
||||
if (!string.IsNullOrWhiteSpace(value)
|
||||
&& !document.SystemProperties.Exists(item => item.Name == displayName))
|
||||
{
|
||||
Add(document, displayName, value, "Registry " + view);
|
||||
}
|
||||
}
|
||||
|
||||
private static string FirstRegistry(RegistryKey key, params string[] names)
|
||||
{
|
||||
foreach (var name in names)
|
||||
{
|
||||
var value = Convert.ToString(key.GetValue(name), CultureInfo.InvariantCulture);
|
||||
if (!string.IsNullOrWhiteSpace(value)) return value.Trim();
|
||||
}
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static string Value(ManagementBaseObject row, string property)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Convert.ToString(row[property], CultureInfo.InvariantCulture) ?? string.Empty;
|
||||
}
|
||||
catch (ManagementException)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private static void Add(InventoryDocument document, string name, string value, string source)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
document.SystemProperties.Add(new NameValueRecord(name, value, source));
|
||||
}
|
||||
}
|
||||
|
||||
private static string WmiDate(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value)) return "Unbekannt";
|
||||
try
|
||||
{
|
||||
return ManagementDateTimeConverter.ToDateTime(value)
|
||||
.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture);
|
||||
}
|
||||
catch (ArgumentOutOfRangeException)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
private static string Unknown(string value)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(value) ? "Nicht ermittelt" : value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace BizTalkApplicationCatalog.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Validiert die Kommandozeile und stellt ausschließlich normalisierte Werte bereit.
|
||||
/// </summary>
|
||||
internal sealed class CommandLineOptions
|
||||
{
|
||||
private static readonly Regex UnsafeEnvironment =
|
||||
new Regex("[^A-Z0-9_-]+", RegexOptions.Compiled | RegexOptions.CultureInvariant);
|
||||
|
||||
public string EnvironmentName { get; private set; }
|
||||
public string OutputDirectory { get; private set; }
|
||||
public string ManagementServer { get; private set; }
|
||||
public string ManagementDatabase { get; private set; }
|
||||
public bool SelfTest { get; private set; }
|
||||
public bool ShowHelp { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Parst die Argumente. Unbekannte oder unvollständige Optionen werden abgelehnt.
|
||||
/// </summary>
|
||||
public static CommandLineOptions Parse(string[] args)
|
||||
{
|
||||
var result = new CommandLineOptions();
|
||||
for (var index = 0; index < args.Length; index++)
|
||||
{
|
||||
var argument = args[index];
|
||||
switch (argument.ToLowerInvariant())
|
||||
{
|
||||
case "--environment":
|
||||
result.EnvironmentName = Value(args, ref index, argument);
|
||||
break;
|
||||
case "--output":
|
||||
result.OutputDirectory = Value(args, ref index, argument);
|
||||
break;
|
||||
case "--management-server":
|
||||
result.ManagementServer = Value(args, ref index, argument);
|
||||
break;
|
||||
case "--management-database":
|
||||
result.ManagementDatabase = Value(args, ref index, argument);
|
||||
break;
|
||||
case "--self-test":
|
||||
result.SelfTest = true;
|
||||
break;
|
||||
case "--help":
|
||||
case "-h":
|
||||
case "/?":
|
||||
result.ShowHelp = true;
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException("Unbekannte Option: " + argument);
|
||||
}
|
||||
}
|
||||
|
||||
if (result.SelfTest || result.ShowHelp)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(result.EnvironmentName))
|
||||
{
|
||||
throw new ArgumentException("--environment fehlt.");
|
||||
}
|
||||
|
||||
result.EnvironmentName = UnsafeEnvironment.Replace(
|
||||
result.EnvironmentName.Trim().ToUpperInvariant(), "-").Trim('-');
|
||||
if (result.EnvironmentName.Length == 0)
|
||||
{
|
||||
throw new ArgumentException("--environment enthält keinen gültigen Namen.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(result.OutputDirectory))
|
||||
{
|
||||
result.OutputDirectory = Path.Combine(
|
||||
Environment.CurrentDirectory, "BizTalk-Anwendungsinventar", result.EnvironmentName);
|
||||
}
|
||||
|
||||
result.OutputDirectory = Path.GetFullPath(
|
||||
Environment.ExpandEnvironmentVariables(result.OutputDirectory));
|
||||
return result;
|
||||
}
|
||||
|
||||
public static string Usage()
|
||||
{
|
||||
return string.Join(Environment.NewLine, new[]
|
||||
{
|
||||
"BEW BizTalk Application Catalog",
|
||||
string.Empty,
|
||||
"Aufruf:",
|
||||
" BizTalkApplicationCatalog.exe --environment ACC [--output PFAD]",
|
||||
" BizTalkApplicationCatalog.exe --environment PROD [--output PFAD]",
|
||||
string.Empty,
|
||||
"Optionen:",
|
||||
" --management-server NAME Optionaler SQL-Server-Hinweis.",
|
||||
" --management-database NAME Optionale Management-Datenbank.",
|
||||
" --self-test Prüft XLSX-Struktur und Kernlogik ohne BizTalk.",
|
||||
" --help Diese Hilfe."
|
||||
});
|
||||
}
|
||||
|
||||
private static string Value(string[] args, ref int index, string option)
|
||||
{
|
||||
index++;
|
||||
if (index >= args.Length || args[index].StartsWith("--", StringComparison.Ordinal))
|
||||
{
|
||||
throw new ArgumentException("Wert für " + option + " fehlt.");
|
||||
}
|
||||
|
||||
return args[index];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace BizTalkApplicationCatalog.Infrastructure
|
||||
{
|
||||
/// <summary>
|
||||
/// Spiegelt alle Fortschrittsmeldungen zeitgleich auf die Konsole und in eine UTF-8-Logdatei.
|
||||
/// </summary>
|
||||
internal sealed class ConsoleFileLogger : IDisposable
|
||||
{
|
||||
private readonly object sync = new object();
|
||||
private readonly StreamWriter writer;
|
||||
|
||||
public ConsoleFileLogger(string path)
|
||||
{
|
||||
writer = new StreamWriter(path, false, new UTF8Encoding(false)) { AutoFlush = true };
|
||||
}
|
||||
|
||||
public void Info(string message) { Write("INFO", message); }
|
||||
public void Warning(string message) { Write("WARNUNG", message); }
|
||||
public void Error(string message) { Write("FEHLER", message); }
|
||||
public void Dispose() { writer.Dispose(); }
|
||||
|
||||
private void Write(string level, string message)
|
||||
{
|
||||
var line = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{0:yyyy-MM-dd HH:mm:ss.fff} [{1}] {2}",
|
||||
DateTime.Now,
|
||||
level,
|
||||
message ?? string.Empty);
|
||||
lock (sync)
|
||||
{
|
||||
Console.WriteLine(line);
|
||||
writer.WriteLine(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using BizTalkApplicationCatalog.Models;
|
||||
|
||||
namespace BizTalkApplicationCatalog.Infrastructure
|
||||
{
|
||||
/// <summary>
|
||||
/// Isoliert Erfassungsfehler, protokolliert sie und lässt unabhängige Abschnitte weiterlaufen.
|
||||
/// </summary>
|
||||
internal sealed class SafeCollector
|
||||
{
|
||||
private readonly InventoryDocument document;
|
||||
private readonly ConsoleFileLogger logger;
|
||||
|
||||
public SafeCollector(InventoryDocument document, ConsoleFileLogger logger)
|
||||
{
|
||||
this.document = document;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public void Execute(string name, bool required, Action action)
|
||||
{
|
||||
var timer = Stopwatch.StartNew();
|
||||
logger.Info("Starte Abschnitt: " + name);
|
||||
try
|
||||
{
|
||||
action();
|
||||
timer.Stop();
|
||||
document.SectionStatuses.Add(new SectionStatus
|
||||
{
|
||||
Name = name,
|
||||
Status = "Erfolgreich",
|
||||
Message = "Abschnitt vollständig ausgeführt.",
|
||||
Required = required,
|
||||
DurationMilliseconds = timer.ElapsedMilliseconds
|
||||
});
|
||||
logger.Info("Abschnitt abgeschlossen: " + name + " (" + timer.ElapsedMilliseconds + " ms)");
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
timer.Stop();
|
||||
document.SectionStatuses.Add(new SectionStatus
|
||||
{
|
||||
Name = name,
|
||||
Status = required ? "Fehler" : "Teilweise",
|
||||
Message = exception.GetType().Name + ": " + exception.Message,
|
||||
Required = required,
|
||||
DurationMilliseconds = timer.ElapsedMilliseconds
|
||||
});
|
||||
document.Findings.Add(new Finding
|
||||
{
|
||||
Severity = required ? "Fehler" : "Warnung",
|
||||
Area = name,
|
||||
Message = "Datenerfassung fehlgeschlagen: " + exception.Message,
|
||||
RecommendedAction = "Logdatei, WMI-Provider und Leseberechtigungen prüfen."
|
||||
});
|
||||
logger.Error(name + ": " + exception.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Xml.Linq;
|
||||
using BizTalkApplicationCatalog.Models;
|
||||
using BizTalkApplicationCatalog.Reporting;
|
||||
|
||||
namespace BizTalkApplicationCatalog.Infrastructure
|
||||
{
|
||||
/// <summary>
|
||||
/// Prüft die zentrale Berichtslogik ohne Zugriff auf Windows- oder BizTalk-WMI.
|
||||
/// </summary>
|
||||
internal static class SelfTestRunner
|
||||
{
|
||||
public static void Run(string outputPath = null)
|
||||
{
|
||||
Assert(SensitiveDataSanitizer.Sanitize(
|
||||
"https://host/path?password=secret&client=100")
|
||||
== "https://host/path?password=[REDACTED]&client=100",
|
||||
"Secret-Redaktion ist fehlerhaft.");
|
||||
|
||||
var keepOutput = !string.IsNullOrWhiteSpace(outputPath);
|
||||
var directory = keepOutput
|
||||
? Path.GetDirectoryName(Path.GetFullPath(outputPath))
|
||||
: Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
"BizTalkApplicationCatalogTests-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(directory);
|
||||
var path = keepOutput ? Path.GetFullPath(outputPath) : Path.Combine(directory, "test.xlsx");
|
||||
try
|
||||
{
|
||||
var document = SampleDocument();
|
||||
new XlsxReportWriter().Write(document, path);
|
||||
Assert(File.Exists(path), "XLSX-Datei wurde nicht erzeugt.");
|
||||
|
||||
using (var archive = ZipFile.OpenRead(path))
|
||||
{
|
||||
var required = new[]
|
||||
{
|
||||
"[Content_Types].xml",
|
||||
"_rels/.rels",
|
||||
"docProps/core.xml",
|
||||
"docProps/app.xml",
|
||||
"xl/workbook.xml",
|
||||
"xl/_rels/workbook.xml.rels",
|
||||
"xl/styles.xml"
|
||||
};
|
||||
foreach (var name in required)
|
||||
{
|
||||
Assert(archive.GetEntry(name) != null, "XLSX-Part fehlt: " + name);
|
||||
}
|
||||
|
||||
for (var index = 1; index <= 10; index++)
|
||||
{
|
||||
Assert(
|
||||
archive.GetEntry("xl/worksheets/sheet" + index + ".xml") != null,
|
||||
"Arbeitsblatt fehlt: " + index);
|
||||
}
|
||||
|
||||
foreach (var entry in archive.Entries.Where(item =>
|
||||
item.FullName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase)
|
||||
|| item.FullName.EndsWith(".rels", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
using (var stream = entry.Open())
|
||||
{
|
||||
XDocument.Load(stream);
|
||||
}
|
||||
}
|
||||
|
||||
var workbook = LoadXml(archive, "xl/workbook.xml");
|
||||
XNamespace spreadsheet =
|
||||
"http://schemas.openxmlformats.org/spreadsheetml/2006/main";
|
||||
var names = workbook.Descendants(spreadsheet + "sheet")
|
||||
.Select(item => (string)item.Attribute("name")).ToList();
|
||||
Assert(names.Contains("Anwendungen"), "Anwendungsblatt fehlt.");
|
||||
Assert(names.Contains("Abdeckung"), "Abdeckungsblatt fehlt.");
|
||||
|
||||
var contentTypes = LoadXml(archive, "[Content_Types].xml");
|
||||
XNamespace contentTypeNamespace =
|
||||
"http://schemas.openxmlformats.org/package/2006/content-types";
|
||||
Assert(
|
||||
contentTypes.Root.Elements(contentTypeNamespace + "Override").Count() == 14,
|
||||
"Content-Type-Overrides sind unvollständig oder im falschen Namespace.");
|
||||
|
||||
var relationships = LoadXml(archive, "_rels/.rels");
|
||||
XNamespace relationshipNamespace =
|
||||
"http://schemas.openxmlformats.org/package/2006/relationships";
|
||||
Assert(
|
||||
relationships.Root.Elements(relationshipNamespace + "Relationship").Count() == 3,
|
||||
"Paketbeziehungen sind unvollständig oder im falschen Namespace.");
|
||||
|
||||
var combinedText = new StringBuilder();
|
||||
foreach (var entry in archive.Entries.Where(item =>
|
||||
item.FullName.StartsWith("xl/worksheets/", StringComparison.Ordinal)))
|
||||
{
|
||||
using (var reader = new StreamReader(entry.Open(), Encoding.UTF8))
|
||||
{
|
||||
combinedText.Append(reader.ReadToEnd());
|
||||
}
|
||||
}
|
||||
Assert(!combinedText.ToString().Contains("supersecret"), "XLSX enthält Testkennwort.");
|
||||
Assert(combinedText.ToString().Contains("[REDACTED]"), "Redaktionsmarker fehlt.");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!keepOutput && Directory.Exists(directory)) Directory.Delete(directory, true);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static InventoryDocument SampleDocument()
|
||||
{
|
||||
var document = new InventoryDocument
|
||||
{
|
||||
EnvironmentName = "ACC",
|
||||
ComputerName = "BIZTALK-ACC",
|
||||
ToolVersion = "1.0.0.0",
|
||||
ManagementServer = "SQL-ACC",
|
||||
ManagementDatabase = "BizTalkMgmtDb",
|
||||
StartedUtc = DateTime.UtcNow.AddSeconds(-1),
|
||||
CompletedUtc = DateTime.UtcNow
|
||||
};
|
||||
document.SystemProperties.Add(new NameValueRecord(
|
||||
"Betriebssystem", "Windows Server 2019", "Self-Test"));
|
||||
document.Applications.Add(new ApplicationRecord
|
||||
{
|
||||
Name = "OrderProcessing",
|
||||
Description = "Aufträge & Sonderzeichen <Test>",
|
||||
Status = "Gestartet (2)",
|
||||
IsDefault = "Nein",
|
||||
Source = "Self-Test"
|
||||
});
|
||||
var port = new ArtifactRecord
|
||||
{
|
||||
Type = "Send Port",
|
||||
ApplicationName = "OrderProcessing",
|
||||
Name = "Send_Orders",
|
||||
Status = "Gestartet (2)",
|
||||
HostName = "SendHost",
|
||||
AdapterName = "WCF-Custom",
|
||||
Address = SensitiveDataSanitizer.Sanitize(
|
||||
"https://service/orders?password=supersecret")
|
||||
};
|
||||
port.Properties.Add(new NameValueRecord("IsTwoWay", "True", "Self-Test"));
|
||||
document.Artifacts.Add(port);
|
||||
document.Coverage.Add(new CoverageRecord
|
||||
{
|
||||
DataSource = "MSBTS_Application",
|
||||
Status = "Vollständig",
|
||||
RowCount = 1,
|
||||
Required = "Ja",
|
||||
Message = "Self-Test"
|
||||
});
|
||||
document.SectionStatuses.Add(new SectionStatus
|
||||
{
|
||||
Name = "Self-Test",
|
||||
Status = "Erfolgreich",
|
||||
Message = "OK",
|
||||
Required = true,
|
||||
DurationMilliseconds = 1
|
||||
});
|
||||
return document;
|
||||
}
|
||||
|
||||
private static XDocument LoadXml(ZipArchive archive, string name)
|
||||
{
|
||||
using (var stream = archive.GetEntry(name).Open())
|
||||
{
|
||||
return XDocument.Load(stream);
|
||||
}
|
||||
}
|
||||
|
||||
private static void Assert(bool condition, string message)
|
||||
{
|
||||
if (!condition) throw new InvalidOperationException(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace BizTalkApplicationCatalog.Infrastructure
|
||||
{
|
||||
/// <summary>
|
||||
/// Verhindert, dass versehentlich Kennwörter oder Token aus WMI-Textwerten in den Bericht gelangen.
|
||||
/// </summary>
|
||||
internal static class SensitiveDataSanitizer
|
||||
{
|
||||
private static readonly Regex NamedSecret = new Regex(
|
||||
@"(?i)(password|passwd|pwd|secret|token|clientsecret|accesskey)\s*([=:])\s*([^;&\s""']+)",
|
||||
RegexOptions.Compiled | RegexOptions.CultureInvariant);
|
||||
|
||||
public static string Sanitize(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return NamedSecret.Replace(value, match =>
|
||||
match.Groups[1].Value + match.Groups[2].Value + "[REDACTED]");
|
||||
}
|
||||
|
||||
public static string RedactProperty(string name, string value)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(name)
|
||||
&& (name.IndexOf("password", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| name.IndexOf("secret", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| name.IndexOf("token", StringComparison.OrdinalIgnoreCase) >= 0))
|
||||
{
|
||||
return "[REDACTED]";
|
||||
}
|
||||
|
||||
return Sanitize(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace BizTalkApplicationCatalog.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Enthält die vollständig normalisierten Ergebnisse eines Inventarlaufs.
|
||||
/// </summary>
|
||||
internal sealed class InventoryDocument
|
||||
{
|
||||
public InventoryDocument()
|
||||
{
|
||||
SystemProperties = new List<NameValueRecord>();
|
||||
Applications = new List<ApplicationRecord>();
|
||||
Artifacts = new List<ArtifactRecord>();
|
||||
Hosts = new List<HostRecord>();
|
||||
Coverage = new List<CoverageRecord>();
|
||||
Findings = new List<Finding>();
|
||||
SectionStatuses = new List<SectionStatus>();
|
||||
}
|
||||
|
||||
public string EnvironmentName { get; set; }
|
||||
public string ComputerName { get; set; }
|
||||
public string ToolVersion { get; set; }
|
||||
public string ManagementServer { get; set; }
|
||||
public string ManagementDatabase { get; set; }
|
||||
public DateTime StartedUtc { get; set; }
|
||||
public DateTime CompletedUtc { get; set; }
|
||||
public List<NameValueRecord> SystemProperties { get; private set; }
|
||||
public List<ApplicationRecord> Applications { get; private set; }
|
||||
public List<ArtifactRecord> Artifacts { get; private set; }
|
||||
public List<HostRecord> Hosts { get; private set; }
|
||||
public List<CoverageRecord> Coverage { get; private set; }
|
||||
public List<Finding> Findings { get; private set; }
|
||||
public List<SectionStatus> SectionStatuses { get; private set; }
|
||||
|
||||
public bool HasRequiredFailure
|
||||
{
|
||||
get
|
||||
{
|
||||
return SectionStatuses.Any(item => item.Required && item.Status != "Erfolgreich")
|
||||
|| Findings.Any(item => item.Severity == "Fehler");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ApplicationRecord
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Status { get; set; }
|
||||
public string IsDefault { get; set; }
|
||||
public string Source { get; set; }
|
||||
}
|
||||
|
||||
internal sealed class ArtifactRecord
|
||||
{
|
||||
public ArtifactRecord()
|
||||
{
|
||||
Properties = new List<NameValueRecord>();
|
||||
}
|
||||
|
||||
public string Type { get; set; }
|
||||
public string ApplicationName { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Status { get; set; }
|
||||
public string HostName { get; set; }
|
||||
public string AdapterName { get; set; }
|
||||
public string ParentName { get; set; }
|
||||
public string Address { get; set; }
|
||||
public List<NameValueRecord> Properties { get; private set; }
|
||||
|
||||
public string Property(string name)
|
||||
{
|
||||
var match = Properties.FirstOrDefault(item =>
|
||||
string.Equals(item.Name, name, StringComparison.OrdinalIgnoreCase));
|
||||
return match == null ? string.Empty : match.Value;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class HostRecord
|
||||
{
|
||||
public string Category { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Server { get; set; }
|
||||
public string Status { get; set; }
|
||||
public string Type { get; set; }
|
||||
public string WindowsGroup { get; set; }
|
||||
public string Is32BitOnly { get; set; }
|
||||
public string Trusted { get; set; }
|
||||
public string AdapterName { get; set; }
|
||||
}
|
||||
|
||||
internal sealed class CoverageRecord
|
||||
{
|
||||
public string DataSource { get; set; }
|
||||
public string Status { get; set; }
|
||||
public int RowCount { get; set; }
|
||||
public string Required { get; set; }
|
||||
public string Message { get; set; }
|
||||
}
|
||||
|
||||
internal sealed class Finding
|
||||
{
|
||||
public string Severity { get; set; }
|
||||
public string Area { get; set; }
|
||||
public string Message { get; set; }
|
||||
public string RecommendedAction { get; set; }
|
||||
}
|
||||
|
||||
internal sealed class SectionStatus
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Status { get; set; }
|
||||
public string Message { get; set; }
|
||||
public bool Required { get; set; }
|
||||
public long DurationMilliseconds { get; set; }
|
||||
}
|
||||
|
||||
internal sealed class NameValueRecord
|
||||
{
|
||||
public NameValueRecord()
|
||||
{
|
||||
}
|
||||
|
||||
public NameValueRecord(string name, string value, string source = "")
|
||||
{
|
||||
Name = name ?? string.Empty;
|
||||
Value = value ?? string.Empty;
|
||||
Source = source ?? string.Empty;
|
||||
}
|
||||
|
||||
public string Name { get; set; }
|
||||
public string Value { get; set; }
|
||||
public string Source { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
using System;
|
||||
using System.Configuration;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using BizTalkApplicationCatalog.Collectors;
|
||||
using BizTalkApplicationCatalog.Configuration;
|
||||
using BizTalkApplicationCatalog.Infrastructure;
|
||||
using BizTalkApplicationCatalog.Models;
|
||||
using BizTalkApplicationCatalog.Reporting;
|
||||
|
||||
namespace BizTalkApplicationCatalog
|
||||
{
|
||||
/// <summary>
|
||||
/// Orchestriert die read-only Erfassung, die Fortschrittsanzeige und den Excel-Export.
|
||||
/// </summary>
|
||||
internal static class Program
|
||||
{
|
||||
private static int Main(string[] args)
|
||||
{
|
||||
CommandLineOptions options;
|
||||
try
|
||||
{
|
||||
options = CommandLineOptions.Parse(args);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Console.Error.WriteLine("FEHLER: " + exception.Message);
|
||||
Console.Error.WriteLine();
|
||||
Console.Error.WriteLine(CommandLineOptions.Usage());
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (options.ShowHelp)
|
||||
{
|
||||
Console.WriteLine(CommandLineOptions.Usage());
|
||||
return 0;
|
||||
}
|
||||
if (options.SelfTest)
|
||||
{
|
||||
return RunSelfTest();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(options.OutputDirectory);
|
||||
}
|
||||
catch (Exception exception) when (
|
||||
exception is IOException
|
||||
|| exception is UnauthorizedAccessException
|
||||
|| exception is ArgumentException)
|
||||
{
|
||||
Console.Error.WriteLine("FEHLER: Ausgabeordner kann nicht erstellt werden: " + exception.Message);
|
||||
return 2;
|
||||
}
|
||||
|
||||
var timestamp = DateTime.Now.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture);
|
||||
var safeMachine = FileName(Environment.MachineName);
|
||||
var baseName = "BizTalk-Anwendungsinventar-"
|
||||
+ options.EnvironmentName + "-" + safeMachine + "-" + timestamp;
|
||||
var logPath = Path.Combine(options.OutputDirectory, baseName + ".log");
|
||||
var reportPath = Path.Combine(options.OutputDirectory, baseName + ".xlsx");
|
||||
|
||||
using (var logger = new ConsoleFileLogger(logPath))
|
||||
{
|
||||
var document = new InventoryDocument
|
||||
{
|
||||
EnvironmentName = options.EnvironmentName,
|
||||
ComputerName = Environment.MachineName,
|
||||
StartedUtc = DateTime.UtcNow
|
||||
};
|
||||
|
||||
logger.Info("BEW BizTalk Application Catalog startet.");
|
||||
logger.Info("Umgebung: " + options.EnvironmentName);
|
||||
logger.Info("Server: " + Environment.MachineName);
|
||||
logger.Info("Ausgabeordner: " + options.OutputDirectory);
|
||||
logger.Info("Modus: ausschließlich lesend; Excel wird ohne Office erzeugt.");
|
||||
|
||||
var safe = new SafeCollector(document, logger);
|
||||
safe.Execute(
|
||||
"System und BizTalk-Gruppe",
|
||||
false,
|
||||
() => new SystemCollector(options).Collect(document));
|
||||
safe.Execute(
|
||||
"Anwendungen und Artefakte",
|
||||
true,
|
||||
() => new BizTalkWmiCollector(
|
||||
document,
|
||||
logger,
|
||||
ReadInt("WmiTimeoutSeconds", 30),
|
||||
ReadInt("MaxRowsPerArtifactType", 10000)).Collect());
|
||||
|
||||
Evaluate(document);
|
||||
document.CompletedUtc = DateTime.UtcNow;
|
||||
try
|
||||
{
|
||||
logger.Info("Erzeuge Microsoft-Excel-Datei: " + reportPath);
|
||||
new XlsxReportWriter().Write(document, reportPath);
|
||||
logger.Info("Excel-Datei erfolgreich erzeugt: " + reportPath);
|
||||
logger.Info("Logdatei: " + logPath);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.Error("Excel-Datei konnte nicht erzeugt werden: " + exception);
|
||||
return 2;
|
||||
}
|
||||
|
||||
logger.Info("Anwendungen: " + document.Applications.Count);
|
||||
logger.Info("Artefakte: " + document.Artifacts.Count);
|
||||
logger.Info("Findings: " + document.Findings.Count);
|
||||
if (document.HasRequiredFailure)
|
||||
{
|
||||
logger.Warning("Inventar wurde mit einem Fehler in einem Pflichtabschnitt erzeugt.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
logger.Info("Inventarisierung erfolgreich abgeschlossen.");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static void Evaluate(InventoryDocument document)
|
||||
{
|
||||
if (document.Applications.Count == 0)
|
||||
{
|
||||
document.Findings.Add(new Finding
|
||||
{
|
||||
Severity = "Fehler",
|
||||
Area = "Anwendungen",
|
||||
Message = "Keine installierte BizTalk-Anwendung ermittelt.",
|
||||
RecommendedAction = "Lokal auf dem BizTalk Server mit ausreichenden WMI-Leserechten ausführen."
|
||||
});
|
||||
}
|
||||
|
||||
var withoutApplication = document.Artifacts.Count(item =>
|
||||
string.IsNullOrWhiteSpace(item.ApplicationName));
|
||||
if (withoutApplication > 0)
|
||||
{
|
||||
document.Findings.Add(new Finding
|
||||
{
|
||||
Severity = "Warnung",
|
||||
Area = "Artefaktzuordnung",
|
||||
Message = withoutApplication + " Artefakt(e) besitzen keine von WMI gelieferte Anwendungszuordnung.",
|
||||
RecommendedAction = "Artefakte im Blatt 'Artefakte' prüfen und bei Bedarf mit der BizTalk Administration Console abgleichen."
|
||||
});
|
||||
}
|
||||
|
||||
if (document.Artifacts.Count == 0)
|
||||
{
|
||||
document.Findings.Add(new Finding
|
||||
{
|
||||
Severity = "Warnung",
|
||||
Area = "Artefakte",
|
||||
Message = "Keine Artefaktdetails wurden ermittelt; die Anwendungsliste kann dennoch vollständig sein.",
|
||||
RecommendedAction = "Blatt 'Abdeckung' sowie WMI-Klassen und Berechtigungen prüfen."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static int RunSelfTest()
|
||||
{
|
||||
try
|
||||
{
|
||||
SelfTestRunner.Run();
|
||||
Console.WriteLine("Self-Test erfolgreich: XLSX-Paket, Tabellen und Secret-Redaktion sind gültig.");
|
||||
return 0;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Console.Error.WriteLine("SELF-TEST FEHLGESCHLAGEN: " + exception);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static int ReadInt(string key, int fallback)
|
||||
{
|
||||
int value;
|
||||
return int.TryParse(
|
||||
ConfigurationManager.AppSettings[key],
|
||||
NumberStyles.Integer,
|
||||
CultureInfo.InvariantCulture,
|
||||
out value)
|
||||
? value
|
||||
: fallback;
|
||||
}
|
||||
|
||||
private static string FileName(string value)
|
||||
{
|
||||
var invalid = Path.GetInvalidFileNameChars();
|
||||
return new string((value ?? "SERVER").Select(character =>
|
||||
invalid.Contains(character) ? '-' : character).ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
[assembly: AssemblyTitle("BEW BizTalk Application Catalog")]
|
||||
[assembly: AssemblyDescription("Read-only BizTalk 2020 application inventory with Microsoft Excel output")]
|
||||
[assembly: AssemblyCompany("JR IT Services")]
|
||||
[assembly: AssemblyProduct("BEW BizTalk Application Catalog")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2026")]
|
||||
[assembly: ComVisible(false)]
|
||||
[assembly: Guid("41cb5701-3fbc-49f4-856a-6ae930b8513d")]
|
||||
[assembly: InternalsVisibleTo("BizTalkApplicationCatalog.Tests")]
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||
@@ -0,0 +1,793 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
using BizTalkApplicationCatalog.Infrastructure;
|
||||
using BizTalkApplicationCatalog.Models;
|
||||
|
||||
namespace BizTalkApplicationCatalog.Reporting
|
||||
{
|
||||
/// <summary>
|
||||
/// Erzeugt eine filterbare Microsoft-Excel-Arbeitsmappe direkt als Office Open XML.
|
||||
/// Excel oder eine Office-Interop-Installation werden nicht benötigt.
|
||||
/// </summary>
|
||||
internal sealed class XlsxReportWriter
|
||||
{
|
||||
private const string SpreadsheetNamespace =
|
||||
"http://schemas.openxmlformats.org/spreadsheetml/2006/main";
|
||||
private const string RelationshipsNamespace =
|
||||
"http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
||||
|
||||
/// <summary>
|
||||
/// Schreibt atomar: Erst nach erfolgreichem Abschluss ersetzt die temporäre Datei das Ziel.
|
||||
/// </summary>
|
||||
public void Write(InventoryDocument document, string outputPath)
|
||||
{
|
||||
if (document == null) throw new ArgumentNullException("document");
|
||||
if (string.IsNullOrWhiteSpace(outputPath)) throw new ArgumentException("Ausgabepfad fehlt.");
|
||||
|
||||
var fullPath = Path.GetFullPath(outputPath);
|
||||
var directory = Path.GetDirectoryName(fullPath);
|
||||
if (!string.IsNullOrWhiteSpace(directory)) Directory.CreateDirectory(directory);
|
||||
var temporaryPath = fullPath + "." + Guid.NewGuid().ToString("N") + ".tmp";
|
||||
|
||||
try
|
||||
{
|
||||
var sheets = BuildSheets(document);
|
||||
using (var archive = ZipFile.Open(temporaryPath, ZipArchiveMode.Create))
|
||||
{
|
||||
WriteContentTypes(archive, sheets.Count);
|
||||
WritePackageRelationships(archive);
|
||||
WriteCoreProperties(archive, document);
|
||||
WriteApplicationProperties(archive);
|
||||
WriteWorkbook(archive, sheets);
|
||||
WriteWorkbookRelationships(archive, sheets.Count);
|
||||
WriteStyles(archive);
|
||||
for (var index = 0; index < sheets.Count; index++)
|
||||
{
|
||||
WriteWorksheet(archive, index + 1, sheets[index]);
|
||||
}
|
||||
}
|
||||
|
||||
if (File.Exists(fullPath)) File.Delete(fullPath);
|
||||
File.Move(temporaryPath, fullPath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(temporaryPath)) File.Delete(temporaryPath);
|
||||
}
|
||||
}
|
||||
|
||||
internal static List<SheetDefinition> BuildSheets(InventoryDocument document)
|
||||
{
|
||||
return new List<SheetDefinition>
|
||||
{
|
||||
BuildOverview(document),
|
||||
BuildApplications(document),
|
||||
BuildArtifacts("Artefakte", document.Artifacts),
|
||||
BuildArtifacts("Ports", document.Artifacts.Where(item =>
|
||||
item.Type == "Send Port"
|
||||
|| item.Type == "Send Port Group"
|
||||
|| item.Type == "Receive Port"
|
||||
|| item.Type == "Receive Location")),
|
||||
BuildArtifacts("Orchestrierungen", document.Artifacts.Where(item =>
|
||||
item.Type == "Orchestrierung")),
|
||||
BuildArtifacts("Schemas-Maps-Pipelines", document.Artifacts.Where(item =>
|
||||
item.Type == "Schema" || item.Type == "Map" || item.Type == "Pipeline")),
|
||||
BuildArtifacts("Assemblies", document.Artifacts.Where(item =>
|
||||
item.Type == "Assembly")),
|
||||
BuildHosts(document),
|
||||
BuildCoverage(document),
|
||||
BuildFindings(document)
|
||||
};
|
||||
}
|
||||
|
||||
private static SheetDefinition BuildOverview(InventoryDocument document)
|
||||
{
|
||||
var rows = new List<object[]>
|
||||
{
|
||||
new object[] { "BEW BizTalk Application Catalog", "" },
|
||||
new object[] { "Umgebung", document.EnvironmentName },
|
||||
new object[] { "BizTalk Server", document.ComputerName },
|
||||
new object[] { "Erzeugt (lokal)", document.CompletedUtc.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture) },
|
||||
new object[] { "Toolversion", document.ToolVersion },
|
||||
new object[] { "Management SQL Server", Empty(document.ManagementServer) },
|
||||
new object[] { "Management Database", Empty(document.ManagementDatabase) },
|
||||
new object[] { "Anwendungen gesamt", document.Applications.Count },
|
||||
new object[] { "Artefakte gesamt", document.Artifacts.Count },
|
||||
new object[] { "Findings", document.Findings.Count },
|
||||
new object[] { "", "" },
|
||||
new object[] { "Artefakttyp", "Anzahl" }
|
||||
};
|
||||
|
||||
foreach (var group in document.Artifacts
|
||||
.GroupBy(item => item.Type)
|
||||
.OrderBy(item => item.Key, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
rows.Add(new object[] { group.Key, group.Count() });
|
||||
}
|
||||
|
||||
rows.Add(new object[] { "", "" });
|
||||
rows.Add(new object[] { "Systemparameter", "Wert", "Quelle" });
|
||||
foreach (var property in document.SystemProperties)
|
||||
{
|
||||
rows.Add(new object[] { property.Name, property.Value, property.Source });
|
||||
}
|
||||
|
||||
rows.Add(new object[] { "", "" });
|
||||
rows.Add(new object[] { "Erfassungsabschnitt", "Status", "Pflicht", "Dauer (ms)", "Meldung" });
|
||||
foreach (var section in document.SectionStatuses)
|
||||
{
|
||||
rows.Add(new object[]
|
||||
{
|
||||
section.Name,
|
||||
section.Status,
|
||||
section.Required ? "Ja" : "Nein",
|
||||
section.DurationMilliseconds,
|
||||
section.Message
|
||||
});
|
||||
}
|
||||
|
||||
return new SheetDefinition("Übersicht", rows, false, 0);
|
||||
}
|
||||
|
||||
private static SheetDefinition BuildApplications(InventoryDocument document)
|
||||
{
|
||||
var headers = new object[]
|
||||
{
|
||||
"Umgebung", "Server", "Anwendung", "Status", "Standard", "Beschreibung",
|
||||
"Artefakte gesamt", "Orchestrierungen", "Send Ports", "Send Port Groups",
|
||||
"Receive Ports", "Receive Locations", "Assemblies", "Schemas", "Maps",
|
||||
"Pipelines", "Hosts", "Adapter", "Detailabdeckung"
|
||||
};
|
||||
var rows = new List<object[]> { headers };
|
||||
var coverage = ArtifactCoverage(document);
|
||||
|
||||
foreach (var application in document.Applications)
|
||||
{
|
||||
var artifacts = document.Artifacts.Where(item =>
|
||||
string.Equals(item.ApplicationName, application.Name, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
rows.Add(new object[]
|
||||
{
|
||||
document.EnvironmentName,
|
||||
document.ComputerName,
|
||||
application.Name,
|
||||
application.Status,
|
||||
application.IsDefault,
|
||||
application.Description,
|
||||
artifacts.Count,
|
||||
Count(artifacts, "Orchestrierung"),
|
||||
Count(artifacts, "Send Port"),
|
||||
Count(artifacts, "Send Port Group"),
|
||||
Count(artifacts, "Receive Port"),
|
||||
Count(artifacts, "Receive Location"),
|
||||
Count(artifacts, "Assembly"),
|
||||
Count(artifacts, "Schema"),
|
||||
Count(artifacts, "Map"),
|
||||
Count(artifacts, "Pipeline"),
|
||||
JoinDistinct(artifacts.Select(item => item.HostName)),
|
||||
JoinDistinct(artifacts.Select(item => item.AdapterName)),
|
||||
coverage
|
||||
});
|
||||
}
|
||||
|
||||
return new SheetDefinition("Anwendungen", rows, true, 1);
|
||||
}
|
||||
|
||||
private static SheetDefinition BuildArtifacts(
|
||||
string sheetName,
|
||||
IEnumerable<ArtifactRecord> source)
|
||||
{
|
||||
var rows = new List<object[]>
|
||||
{
|
||||
new object[]
|
||||
{
|
||||
"Anwendung", "Typ", "Name", "Status", "Host/Handler", "Adapter",
|
||||
"Übergeordnet", "Adresse", "Beschreibung", "Two-Way", "Dynamisch",
|
||||
"Deaktiviert", "Receive Pipeline", "Send Pipeline", "Secondary Adapter",
|
||||
"Secondary Adresse", "Assembly/FullName", "Namespace", "Root", "Tracking"
|
||||
}
|
||||
};
|
||||
foreach (var item in source)
|
||||
{
|
||||
rows.Add(new object[]
|
||||
{
|
||||
item.ApplicationName,
|
||||
item.Type,
|
||||
item.Name,
|
||||
item.Status,
|
||||
item.HostName,
|
||||
item.AdapterName,
|
||||
item.ParentName,
|
||||
item.Address,
|
||||
item.Property("Description"),
|
||||
item.Property("IsTwoWay"),
|
||||
item.Property("IsDynamic"),
|
||||
item.Property("IsDisabled"),
|
||||
item.Property("ReceivePipeline"),
|
||||
item.Property("SendPipeline"),
|
||||
item.Property("STTransportType"),
|
||||
SensitiveDataSanitizer.Sanitize(item.Property("STAddress")),
|
||||
FirstNonEmpty(item.Property("AssemblyName"), item.Property("FullName")),
|
||||
item.Property("TargetNameSpace"),
|
||||
item.Property("RootName"),
|
||||
item.Property("Tracking")
|
||||
});
|
||||
}
|
||||
return new SheetDefinition(sheetName, rows, true, 1);
|
||||
}
|
||||
|
||||
private static SheetDefinition BuildHosts(InventoryDocument document)
|
||||
{
|
||||
var rows = new List<object[]>
|
||||
{
|
||||
new object[]
|
||||
{
|
||||
"Kategorie", "Name", "Server", "Status", "Typ", "Windows-Gruppe",
|
||||
"Nur 32 Bit", "Vertrauenswürdig", "Adapter"
|
||||
}
|
||||
};
|
||||
foreach (var item in document.Hosts)
|
||||
{
|
||||
rows.Add(new object[]
|
||||
{
|
||||
item.Category, item.Name, item.Server, item.Status, item.Type,
|
||||
item.WindowsGroup, item.Is32BitOnly, item.Trusted, item.AdapterName
|
||||
});
|
||||
}
|
||||
return new SheetDefinition("Hosts-Handler", rows, true, 1);
|
||||
}
|
||||
|
||||
private static SheetDefinition BuildCoverage(InventoryDocument document)
|
||||
{
|
||||
var rows = new List<object[]>
|
||||
{
|
||||
new object[] { "Datenquelle", "Status", "Zeilen", "Pflicht", "Meldung" }
|
||||
};
|
||||
foreach (var item in document.Coverage)
|
||||
{
|
||||
rows.Add(new object[]
|
||||
{
|
||||
item.DataSource, item.Status, item.RowCount, item.Required, item.Message
|
||||
});
|
||||
}
|
||||
return new SheetDefinition("Abdeckung", rows, true, 1);
|
||||
}
|
||||
|
||||
private static SheetDefinition BuildFindings(InventoryDocument document)
|
||||
{
|
||||
var rows = new List<object[]>
|
||||
{
|
||||
new object[] { "Schweregrad", "Bereich", "Feststellung", "Empfohlene Aktion" }
|
||||
};
|
||||
foreach (var item in document.Findings)
|
||||
{
|
||||
rows.Add(new object[]
|
||||
{
|
||||
item.Severity, item.Area, item.Message, item.RecommendedAction
|
||||
});
|
||||
}
|
||||
if (document.Findings.Count == 0)
|
||||
{
|
||||
rows.Add(new object[] { "Information", "Gesamt", "Keine Findings.", "" });
|
||||
}
|
||||
return new SheetDefinition("Findings", rows, true, 1);
|
||||
}
|
||||
|
||||
private static string ArtifactCoverage(InventoryDocument document)
|
||||
{
|
||||
var incomplete = document.Coverage
|
||||
.Where(item => item.DataSource.StartsWith("MSBTS_", StringComparison.Ordinal)
|
||||
&& item.DataSource != "MSBTS_Application"
|
||||
&& item.Status != "Vollständig")
|
||||
.Select(item => item.DataSource + ": " + item.Status)
|
||||
.ToList();
|
||||
return incomplete.Count == 0
|
||||
? "Vollständig"
|
||||
: "Teilweise – siehe Abdeckung: " + string.Join(", ", incomplete);
|
||||
}
|
||||
|
||||
private static int Count(IEnumerable<ArtifactRecord> artifacts, string type)
|
||||
{
|
||||
return artifacts.Count(item => item.Type == type);
|
||||
}
|
||||
|
||||
private static string JoinDistinct(IEnumerable<string> values)
|
||||
{
|
||||
return string.Join(
|
||||
", ",
|
||||
values.Where(item => !string.IsNullOrWhiteSpace(item))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(item => item, StringComparer.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static string FirstNonEmpty(params string[] values)
|
||||
{
|
||||
return values.FirstOrDefault(item => !string.IsNullOrWhiteSpace(item)) ?? string.Empty;
|
||||
}
|
||||
|
||||
private static string Empty(string value)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(value) ? "Nicht ermittelt" : value;
|
||||
}
|
||||
|
||||
private static void WriteWorksheet(ZipArchive archive, int sheetNumber, SheetDefinition sheet)
|
||||
{
|
||||
using (var writer = CreateXmlWriter(archive, "xl/worksheets/sheet" + sheetNumber + ".xml"))
|
||||
{
|
||||
writer.WriteStartDocument();
|
||||
writer.WriteStartElement("worksheet", SpreadsheetNamespace);
|
||||
writer.WriteAttributeString("xmlns", "r", null, RelationshipsNamespace);
|
||||
WriteSheetViews(writer, sheet.FreezeRows);
|
||||
WriteColumns(writer, sheet);
|
||||
writer.WriteStartElement("sheetData", SpreadsheetNamespace);
|
||||
|
||||
for (var rowIndex = 0; rowIndex < sheet.Rows.Count; rowIndex++)
|
||||
{
|
||||
writer.WriteStartElement("row", SpreadsheetNamespace);
|
||||
writer.WriteAttributeString("r", (rowIndex + 1).ToString(CultureInfo.InvariantCulture));
|
||||
for (var columnIndex = 0; columnIndex < sheet.Rows[rowIndex].Length; columnIndex++)
|
||||
{
|
||||
var style = RowStyle(sheet, rowIndex);
|
||||
WriteCell(
|
||||
writer,
|
||||
columnIndex + 1,
|
||||
rowIndex + 1,
|
||||
sheet.Rows[rowIndex][columnIndex],
|
||||
style);
|
||||
}
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
writer.WriteEndElement();
|
||||
|
||||
if (sheet.AutoFilter && sheet.Rows.Count > 0 && sheet.MaximumColumns > 0)
|
||||
{
|
||||
writer.WriteStartElement("autoFilter", SpreadsheetNamespace);
|
||||
writer.WriteAttributeString(
|
||||
"ref",
|
||||
"A1:" + ColumnName(sheet.MaximumColumns) + sheet.Rows.Count);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
writer.WriteStartElement("pageMargins", SpreadsheetNamespace);
|
||||
writer.WriteAttributeString("left", "0.25");
|
||||
writer.WriteAttributeString("right", "0.25");
|
||||
writer.WriteAttributeString("top", "0.5");
|
||||
writer.WriteAttributeString("bottom", "0.5");
|
||||
writer.WriteAttributeString("header", "0.2");
|
||||
writer.WriteAttributeString("footer", "0.2");
|
||||
writer.WriteEndElement();
|
||||
writer.WriteEndElement();
|
||||
writer.WriteEndDocument();
|
||||
}
|
||||
}
|
||||
|
||||
private static int RowStyle(SheetDefinition sheet, int rowIndex)
|
||||
{
|
||||
if (sheet.Name == "Übersicht")
|
||||
{
|
||||
if (rowIndex == 0) return 2;
|
||||
var first = Convert.ToString(sheet.Rows[rowIndex].FirstOrDefault(), CultureInfo.InvariantCulture);
|
||||
if (first == "Artefakttyp"
|
||||
|| first == "Systemparameter"
|
||||
|| first == "Erfassungsabschnitt") return 1;
|
||||
return 0;
|
||||
}
|
||||
return rowIndex == 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
private static void WriteSheetViews(XmlWriter writer, int freezeRows)
|
||||
{
|
||||
writer.WriteStartElement("sheetViews", SpreadsheetNamespace);
|
||||
writer.WriteStartElement("sheetView", SpreadsheetNamespace);
|
||||
writer.WriteAttributeString("workbookViewId", "0");
|
||||
if (freezeRows > 0)
|
||||
{
|
||||
writer.WriteStartElement("pane", SpreadsheetNamespace);
|
||||
writer.WriteAttributeString("ySplit", freezeRows.ToString(CultureInfo.InvariantCulture));
|
||||
writer.WriteAttributeString("topLeftCell", "A" + (freezeRows + 1));
|
||||
writer.WriteAttributeString("activePane", "bottomLeft");
|
||||
writer.WriteAttributeString("state", "frozen");
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
writer.WriteEndElement();
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
private static void WriteColumns(XmlWriter writer, SheetDefinition sheet)
|
||||
{
|
||||
writer.WriteStartElement("cols", SpreadsheetNamespace);
|
||||
for (var columnIndex = 0; columnIndex < sheet.MaximumColumns; columnIndex++)
|
||||
{
|
||||
var width = 10;
|
||||
foreach (var row in sheet.Rows)
|
||||
{
|
||||
if (columnIndex >= row.Length) continue;
|
||||
var length = Convert.ToString(row[columnIndex], CultureInfo.InvariantCulture).Length + 2;
|
||||
width = Math.Max(width, Math.Min(60, length));
|
||||
}
|
||||
writer.WriteStartElement("col", SpreadsheetNamespace);
|
||||
writer.WriteAttributeString("min", (columnIndex + 1).ToString(CultureInfo.InvariantCulture));
|
||||
writer.WriteAttributeString("max", (columnIndex + 1).ToString(CultureInfo.InvariantCulture));
|
||||
writer.WriteAttributeString("width", width.ToString(CultureInfo.InvariantCulture));
|
||||
writer.WriteAttributeString("customWidth", "1");
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
private static void WriteCell(
|
||||
XmlWriter writer,
|
||||
int column,
|
||||
int row,
|
||||
object value,
|
||||
int style)
|
||||
{
|
||||
writer.WriteStartElement("c", SpreadsheetNamespace);
|
||||
writer.WriteAttributeString("r", ColumnName(column) + row);
|
||||
if (style > 0) writer.WriteAttributeString("s", style.ToString(CultureInfo.InvariantCulture));
|
||||
if (IsNumber(value))
|
||||
{
|
||||
writer.WriteStartElement("v", SpreadsheetNamespace);
|
||||
writer.WriteString(Convert.ToString(value, CultureInfo.InvariantCulture));
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteAttributeString("t", "inlineStr");
|
||||
writer.WriteStartElement("is", SpreadsheetNamespace);
|
||||
writer.WriteStartElement("t", SpreadsheetNamespace);
|
||||
writer.WriteAttributeString("xml", "space", null, "preserve");
|
||||
writer.WriteString(ExcelText(Convert.ToString(value, CultureInfo.InvariantCulture)));
|
||||
writer.WriteEndElement();
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
private static bool IsNumber(object value)
|
||||
{
|
||||
return value is byte || value is short || value is int || value is long
|
||||
|| value is float || value is double || value is decimal;
|
||||
}
|
||||
|
||||
private static string ExcelText(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) return string.Empty;
|
||||
var builder = new StringBuilder(Math.Min(value.Length, 32767));
|
||||
foreach (var character in value)
|
||||
{
|
||||
if (builder.Length >= 32767) break;
|
||||
if (XmlConvert.IsXmlChar(character)) builder.Append(character);
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static string ColumnName(int number)
|
||||
{
|
||||
var result = string.Empty;
|
||||
while (number > 0)
|
||||
{
|
||||
number--;
|
||||
result = (char)('A' + (number % 26)) + result;
|
||||
number /= 26;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void WriteContentTypes(ZipArchive archive, int sheetCount)
|
||||
{
|
||||
using (var writer = CreateXmlWriter(archive, "[Content_Types].xml"))
|
||||
{
|
||||
writer.WriteStartDocument();
|
||||
writer.WriteStartElement("Types", "http://schemas.openxmlformats.org/package/2006/content-types");
|
||||
WriteDefault(writer, "rels", "application/vnd.openxmlformats-package.relationships+xml");
|
||||
WriteDefault(writer, "xml", "application/xml");
|
||||
WriteOverride(writer, "/xl/workbook.xml", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml");
|
||||
WriteOverride(writer, "/xl/styles.xml", "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml");
|
||||
WriteOverride(writer, "/docProps/core.xml", "application/vnd.openxmlformats-package.core-properties+xml");
|
||||
WriteOverride(writer, "/docProps/app.xml", "application/vnd.openxmlformats-officedocument.extended-properties+xml");
|
||||
for (var index = 1; index <= sheetCount; index++)
|
||||
{
|
||||
WriteOverride(
|
||||
writer,
|
||||
"/xl/worksheets/sheet" + index + ".xml",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml");
|
||||
}
|
||||
writer.WriteEndElement();
|
||||
writer.WriteEndDocument();
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteDefault(XmlWriter writer, string extension, string type)
|
||||
{
|
||||
writer.WriteStartElement("Default");
|
||||
writer.WriteAttributeString("Extension", extension);
|
||||
writer.WriteAttributeString("ContentType", type);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
private static void WriteOverride(XmlWriter writer, string partName, string type)
|
||||
{
|
||||
writer.WriteStartElement("Override");
|
||||
writer.WriteAttributeString("PartName", partName);
|
||||
writer.WriteAttributeString("ContentType", type);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
private static void WritePackageRelationships(ZipArchive archive)
|
||||
{
|
||||
using (var writer = CreateXmlWriter(archive, "_rels/.rels"))
|
||||
{
|
||||
writer.WriteStartDocument();
|
||||
writer.WriteStartElement("Relationships", "http://schemas.openxmlformats.org/package/2006/relationships");
|
||||
WriteRelationship(writer, "rId1", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument", "xl/workbook.xml");
|
||||
WriteRelationship(writer, "rId2", "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties", "docProps/core.xml");
|
||||
WriteRelationship(writer, "rId3", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties", "docProps/app.xml");
|
||||
writer.WriteEndElement();
|
||||
writer.WriteEndDocument();
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteWorkbook(ZipArchive archive, List<SheetDefinition> sheets)
|
||||
{
|
||||
using (var writer = CreateXmlWriter(archive, "xl/workbook.xml"))
|
||||
{
|
||||
writer.WriteStartDocument();
|
||||
writer.WriteStartElement("workbook", SpreadsheetNamespace);
|
||||
writer.WriteAttributeString("xmlns", "r", null, RelationshipsNamespace);
|
||||
writer.WriteStartElement("bookViews", SpreadsheetNamespace);
|
||||
writer.WriteStartElement("workbookView", SpreadsheetNamespace);
|
||||
writer.WriteAttributeString("activeTab", "0");
|
||||
writer.WriteEndElement();
|
||||
writer.WriteEndElement();
|
||||
writer.WriteStartElement("sheets", SpreadsheetNamespace);
|
||||
for (var index = 0; index < sheets.Count; index++)
|
||||
{
|
||||
writer.WriteStartElement("sheet", SpreadsheetNamespace);
|
||||
writer.WriteAttributeString("name", sheets[index].Name);
|
||||
writer.WriteAttributeString("sheetId", (index + 1).ToString(CultureInfo.InvariantCulture));
|
||||
writer.WriteAttributeString("r", "id", RelationshipsNamespace, "rId" + (index + 1));
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
writer.WriteEndElement();
|
||||
writer.WriteEndElement();
|
||||
writer.WriteEndDocument();
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteWorkbookRelationships(ZipArchive archive, int sheetCount)
|
||||
{
|
||||
using (var writer = CreateXmlWriter(archive, "xl/_rels/workbook.xml.rels"))
|
||||
{
|
||||
writer.WriteStartDocument();
|
||||
writer.WriteStartElement("Relationships", "http://schemas.openxmlformats.org/package/2006/relationships");
|
||||
for (var index = 1; index <= sheetCount; index++)
|
||||
{
|
||||
WriteRelationship(
|
||||
writer,
|
||||
"rId" + index,
|
||||
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet",
|
||||
"worksheets/sheet" + index + ".xml");
|
||||
}
|
||||
WriteRelationship(
|
||||
writer,
|
||||
"rId" + (sheetCount + 1),
|
||||
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles",
|
||||
"styles.xml");
|
||||
writer.WriteEndElement();
|
||||
writer.WriteEndDocument();
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteRelationship(XmlWriter writer, string id, string type, string target)
|
||||
{
|
||||
writer.WriteStartElement("Relationship");
|
||||
writer.WriteAttributeString("Id", id);
|
||||
writer.WriteAttributeString("Type", type);
|
||||
writer.WriteAttributeString("Target", target);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
private static void WriteStyles(ZipArchive archive)
|
||||
{
|
||||
using (var writer = CreateXmlWriter(archive, "xl/styles.xml"))
|
||||
{
|
||||
writer.WriteStartDocument();
|
||||
writer.WriteStartElement("styleSheet", SpreadsheetNamespace);
|
||||
writer.WriteStartElement("fonts", SpreadsheetNamespace);
|
||||
writer.WriteAttributeString("count", "3");
|
||||
WriteFont(writer, false, "000000", 10);
|
||||
WriteFont(writer, true, "FFFFFF", 10);
|
||||
WriteFont(writer, true, "FFFFFF", 16);
|
||||
writer.WriteEndElement();
|
||||
writer.WriteStartElement("fills", SpreadsheetNamespace);
|
||||
writer.WriteAttributeString("count", "4");
|
||||
WritePatternFill(writer, "none", null);
|
||||
WritePatternFill(writer, "gray125", null);
|
||||
WritePatternFill(writer, "solid", "1F4E78");
|
||||
WritePatternFill(writer, "solid", "2F75B5");
|
||||
writer.WriteEndElement();
|
||||
writer.WriteStartElement("borders", SpreadsheetNamespace);
|
||||
writer.WriteAttributeString("count", "2");
|
||||
WriteBorder(writer, false);
|
||||
WriteBorder(writer, true);
|
||||
writer.WriteEndElement();
|
||||
writer.WriteStartElement("cellStyleXfs", SpreadsheetNamespace);
|
||||
writer.WriteAttributeString("count", "1");
|
||||
WriteXf(writer, 0, 0, 0, false);
|
||||
writer.WriteEndElement();
|
||||
writer.WriteStartElement("cellXfs", SpreadsheetNamespace);
|
||||
writer.WriteAttributeString("count", "3");
|
||||
WriteXf(writer, 0, 0, 0, true);
|
||||
WriteXf(writer, 1, 2, 1, true);
|
||||
WriteXf(writer, 2, 3, 1, true);
|
||||
writer.WriteEndElement();
|
||||
writer.WriteStartElement("cellStyles", SpreadsheetNamespace);
|
||||
writer.WriteAttributeString("count", "1");
|
||||
writer.WriteStartElement("cellStyle", SpreadsheetNamespace);
|
||||
writer.WriteAttributeString("name", "Normal");
|
||||
writer.WriteAttributeString("xfId", "0");
|
||||
writer.WriteAttributeString("builtinId", "0");
|
||||
writer.WriteEndElement();
|
||||
writer.WriteEndElement();
|
||||
writer.WriteEndElement();
|
||||
writer.WriteEndDocument();
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteFont(XmlWriter writer, bool bold, string color, int size)
|
||||
{
|
||||
writer.WriteStartElement("font", SpreadsheetNamespace);
|
||||
if (bold) writer.WriteElementString("b", SpreadsheetNamespace, string.Empty);
|
||||
writer.WriteStartElement("sz", SpreadsheetNamespace);
|
||||
writer.WriteAttributeString("val", size.ToString(CultureInfo.InvariantCulture));
|
||||
writer.WriteEndElement();
|
||||
writer.WriteStartElement("color", SpreadsheetNamespace);
|
||||
writer.WriteAttributeString("rgb", "FF" + color);
|
||||
writer.WriteEndElement();
|
||||
writer.WriteStartElement("name", SpreadsheetNamespace);
|
||||
writer.WriteAttributeString("val", "Calibri");
|
||||
writer.WriteEndElement();
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
private static void WritePatternFill(XmlWriter writer, string pattern, string color)
|
||||
{
|
||||
writer.WriteStartElement("fill", SpreadsheetNamespace);
|
||||
writer.WriteStartElement("patternFill", SpreadsheetNamespace);
|
||||
writer.WriteAttributeString("patternType", pattern);
|
||||
if (color != null)
|
||||
{
|
||||
writer.WriteStartElement("fgColor", SpreadsheetNamespace);
|
||||
writer.WriteAttributeString("rgb", "FF" + color);
|
||||
writer.WriteEndElement();
|
||||
writer.WriteStartElement("bgColor", SpreadsheetNamespace);
|
||||
writer.WriteAttributeString("indexed", "64");
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
writer.WriteEndElement();
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
private static void WriteBorder(XmlWriter writer, bool thin)
|
||||
{
|
||||
writer.WriteStartElement("border", SpreadsheetNamespace);
|
||||
foreach (var side in new[] { "left", "right", "top", "bottom" })
|
||||
{
|
||||
writer.WriteStartElement(side, SpreadsheetNamespace);
|
||||
if (thin)
|
||||
{
|
||||
writer.WriteAttributeString("style", "thin");
|
||||
writer.WriteStartElement("color", SpreadsheetNamespace);
|
||||
writer.WriteAttributeString("rgb", "FFD9E2F3");
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
writer.WriteElementString("diagonal", SpreadsheetNamespace, string.Empty);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
private static void WriteXf(
|
||||
XmlWriter writer,
|
||||
int fontId,
|
||||
int fillId,
|
||||
int borderId,
|
||||
bool alignment)
|
||||
{
|
||||
writer.WriteStartElement("xf", SpreadsheetNamespace);
|
||||
writer.WriteAttributeString("numFmtId", "0");
|
||||
writer.WriteAttributeString("fontId", fontId.ToString(CultureInfo.InvariantCulture));
|
||||
writer.WriteAttributeString("fillId", fillId.ToString(CultureInfo.InvariantCulture));
|
||||
writer.WriteAttributeString("borderId", borderId.ToString(CultureInfo.InvariantCulture));
|
||||
writer.WriteAttributeString("xfId", "0");
|
||||
if (fontId > 0) writer.WriteAttributeString("applyFont", "1");
|
||||
if (fillId > 0) writer.WriteAttributeString("applyFill", "1");
|
||||
if (borderId > 0) writer.WriteAttributeString("applyBorder", "1");
|
||||
if (alignment)
|
||||
{
|
||||
writer.WriteAttributeString("applyAlignment", "1");
|
||||
writer.WriteStartElement("alignment", SpreadsheetNamespace);
|
||||
writer.WriteAttributeString("vertical", "top");
|
||||
writer.WriteAttributeString("wrapText", "1");
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
private static void WriteCoreProperties(ZipArchive archive, InventoryDocument document)
|
||||
{
|
||||
using (var writer = CreateXmlWriter(archive, "docProps/core.xml"))
|
||||
{
|
||||
writer.WriteStartDocument();
|
||||
writer.WriteStartElement("cp", "coreProperties", "http://schemas.openxmlformats.org/package/2006/metadata/core-properties");
|
||||
writer.WriteAttributeString("xmlns", "dc", null, "http://purl.org/dc/elements/1.1/");
|
||||
writer.WriteAttributeString("xmlns", "dcterms", null, "http://purl.org/dc/terms/");
|
||||
writer.WriteAttributeString("xmlns", "xsi", null, "http://www.w3.org/2001/XMLSchema-instance");
|
||||
writer.WriteElementString("dc", "title", "http://purl.org/dc/elements/1.1/", "BizTalk-Anwendungsinventar " + document.EnvironmentName);
|
||||
writer.WriteElementString("dc", "creator", "http://purl.org/dc/elements/1.1/", "BEW BizTalk Application Catalog");
|
||||
writer.WriteElementString("cp", "lastModifiedBy", "http://schemas.openxmlformats.org/package/2006/metadata/core-properties", "BEW BizTalk Application Catalog");
|
||||
writer.WriteStartElement("dcterms", "created", "http://purl.org/dc/terms/");
|
||||
writer.WriteAttributeString("xsi", "type", "http://www.w3.org/2001/XMLSchema-instance", "dcterms:W3CDTF");
|
||||
writer.WriteString(document.CompletedUtc.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture));
|
||||
writer.WriteEndElement();
|
||||
writer.WriteEndElement();
|
||||
writer.WriteEndDocument();
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteApplicationProperties(ZipArchive archive)
|
||||
{
|
||||
using (var writer = CreateXmlWriter(archive, "docProps/app.xml"))
|
||||
{
|
||||
writer.WriteStartDocument();
|
||||
writer.WriteStartElement("Properties", "http://schemas.openxmlformats.org/officeDocument/2006/extended-properties");
|
||||
writer.WriteAttributeString("xmlns", "vt", null, "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes");
|
||||
writer.WriteElementString("Application", "BEW BizTalk Application Catalog");
|
||||
writer.WriteElementString("AppVersion", "1.0");
|
||||
writer.WriteEndElement();
|
||||
writer.WriteEndDocument();
|
||||
}
|
||||
}
|
||||
|
||||
private static XmlWriter CreateXmlWriter(ZipArchive archive, string path)
|
||||
{
|
||||
var entry = archive.CreateEntry(path, CompressionLevel.Optimal);
|
||||
return XmlWriter.Create(entry.Open(), new XmlWriterSettings
|
||||
{
|
||||
Encoding = new UTF8Encoding(false),
|
||||
Indent = false,
|
||||
CloseOutput = true
|
||||
});
|
||||
}
|
||||
|
||||
internal sealed class SheetDefinition
|
||||
{
|
||||
public SheetDefinition(
|
||||
string name,
|
||||
List<object[]> rows,
|
||||
bool autoFilter,
|
||||
int freezeRows)
|
||||
{
|
||||
Name = name;
|
||||
Rows = rows;
|
||||
AutoFilter = autoFilter;
|
||||
FreezeRows = freezeRows;
|
||||
}
|
||||
|
||||
public string Name { get; private set; }
|
||||
public List<object[]> Rows { get; private set; }
|
||||
public bool AutoFilter { get; private set; }
|
||||
public int FreezeRows { get; private set; }
|
||||
public int MaximumColumns
|
||||
{
|
||||
get { return Rows.Count == 0 ? 0 : Rows.Max(item => item.Length); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user