Initial commit: BizTalk IIS inventory with DOCX reporting
Build und Test / build (push) Has been cancelled

This commit is contained in:
2026-07-24 15:32:18 +02:00
commit d326341488
27 changed files with 4776 additions and 0 deletions
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<appSettings>
<!-- Obergrenze fuer das Dateimanifest je IIS-Anwendung. -->
<add key="MaxFilesPerApplication" value="10000" />
<!-- Maximale Rekursionstiefe unterhalb eines Web-Stammverzeichnisses. -->
<add key="MaxContentDepth" value="30" />
<!-- SHA-256 fuer Webdateien ist genauer, kann aber auf grossen Verzeichnissen teuer sein. -->
<add key="IncludeFileHashes" value="false" />
<!-- Alle Zertifikate aus LocalMachine\My dokumentieren; Bindungszertifikate immer. -->
<add key="IncludeAllPersonalCertificates" value="true" />
<!-- Timeout fuer jede WMI-Abfrage. -->
<add key="WmiTimeoutSeconds" value="30" />
<!-- Erwarteter BEW-Scope; leer lassen, um die Vollstaendigkeitspruefung zu deaktivieren. -->
<add key="ExpectedApplications" value="heat_archive,heat_bankdata,heat_bbill,heat_caccount,heat_invoice,heat_meterchange,heat_meterlist,heat_meterreading" />
</appSettings>
<startup useLegacyV2RuntimeActivationPolicy="true">
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
</configuration>
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net472</TargetFramework>
<RootNamespace>BizTalkIisEnvironmentInventory</RootNamespace>
<AssemblyName>BizTalkIisEnvironmentInventory</AssemblyName>
<LangVersion>7.3</LangVersion>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
<Deterministic>true</Deterministic>
<PlatformTarget>AnyCPU</PlatformTarget>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<ItemGroup>
<Reference Include="System.Configuration" />
<Reference Include="System.IO.Compression" />
<Reference Include="System.IO.Compression.FileSystem" />
<Reference Include="System.Management" />
<Reference Include="System.Security" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="All" />
</ItemGroup>
</Project>
@@ -0,0 +1,260 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Management;
using Microsoft.Win32;
using BizTalkIisEnvironmentInventory.Configuration;
using BizTalkIisEnvironmentInventory.Infrastructure;
using BizTalkIisEnvironmentInventory.Models;
namespace BizTalkIisEnvironmentInventory.Collectors
{
/// <summary>
/// Erfasst BizTalk-Installation, Registrymetadaten, Binärversionen und WMI-Komponentenzahlen.
/// </summary>
internal sealed class BizTalkCollector
{
private readonly CollectorOptions options;
/// <summary>
/// Initialisiert den BizTalk-Collector.
/// </summary>
/// <param name="options">WMI-Timeout.</param>
public BizTalkCollector(CollectorOptions options)
{
this.options = options;
}
/// <summary>
/// Erfasst lokale BizTalk-Komponenten ohne ExplorerOM-Abhängigkeit.
/// </summary>
/// <param name="target">BizTalk-Zielmodell.</param>
/// <param name="findings">Liste nicht fataler Auffälligkeiten.</param>
public void Collect(BizTalkInventory target, IList<Finding> findings)
{
ReadBizTalkRegistry(target);
ReadInstalledProducts(target);
ReadComponentVersions(target, findings);
ReadBizTalkWmi(target, findings);
}
/// <summary>
/// Liest nicht sensible Werte aus dem BizTalk-Hauptschlüssel in beiden Registry Views.
/// </summary>
/// <param name="target">BizTalk-Zielmodell.</param>
private static void ReadBizTalkRegistry(BizTalkInventory target)
{
foreach (var view in new[] { RegistryView.Registry64, RegistryView.Registry32 })
{
using (var baseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, view))
using (var key = baseKey.OpenSubKey(@"SOFTWARE\Microsoft\BizTalk Server\3.0", false))
{
if (key == null)
{
continue;
}
foreach (var name in key.GetValueNames().OrderBy(item => item, StringComparer.OrdinalIgnoreCase))
{
if (SensitiveDataSanitizer.IsSensitiveName(name))
{
target.RegistryValues.Add(new NameValueRecord(view + " / " + name, "[ENTFERNT]"));
continue;
}
var value = key.GetValue(name);
target.RegistryValues.Add(new NameValueRecord(
view + " / " + name,
Convert.ToString(value, CultureInfo.InvariantCulture)));
}
}
}
}
/// <summary>
/// Liest BizTalk-bezogene Einträge aus den Windows-Uninstall-Schlüsseln.
/// </summary>
/// <param name="target">BizTalk-Zielmodell.</param>
private static void ReadInstalledProducts(BizTalkInventory target)
{
foreach (var view in new[] { RegistryView.Registry64, RegistryView.Registry32 })
{
using (var baseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, view))
using (var uninstall = baseKey.OpenSubKey(
@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall",
false))
{
if (uninstall == null)
{
continue;
}
foreach (var childName in uninstall.GetSubKeyNames())
{
using (var child = uninstall.OpenSubKey(childName, false))
{
var displayName = Convert.ToString(child == null ? null : child.GetValue("DisplayName"));
if (displayName.IndexOf("BizTalk", StringComparison.OrdinalIgnoreCase) < 0
&& displayName.IndexOf("Enterprise Single Sign-On", StringComparison.OrdinalIgnoreCase) < 0)
{
continue;
}
var version = Convert.ToString(child.GetValue("DisplayVersion"));
var date = Convert.ToString(child.GetValue("InstallDate"));
target.InstalledProducts.Add(new NameValueRecord(
displayName,
version + (string.IsNullOrWhiteSpace(date) ? string.Empty : " | InstallDate " + date)));
}
}
}
}
target.InstalledProducts.Sort((left, right) =>
string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase));
}
/// <summary>
/// Liest Dateiversionen zentraler BizTalk-Binärdateien aus dem Installationspfad.
/// </summary>
/// <param name="target">BizTalk-Zielmodell.</param>
/// <param name="findings">Liste nicht fataler Dateizugriffsfehler.</param>
private static void ReadComponentVersions(BizTalkInventory target, IList<Finding> findings)
{
var paths = target.RegistryValues
.Where(item => item.Name.IndexOf("InstallPath", StringComparison.OrdinalIgnoreCase) >= 0
|| item.Name.IndexOf("InstallDir", StringComparison.OrdinalIgnoreCase) >= 0
|| item.Name.IndexOf("ProductPath", StringComparison.OrdinalIgnoreCase) >= 0)
.Select(item => Environment.ExpandEnvironmentVariables(item.Value ?? string.Empty))
.Where(Directory.Exists)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
if (paths.Count == 0)
{
var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
var fallback = Path.Combine(programFiles, "Microsoft BizTalk Server");
if (Directory.Exists(fallback))
{
paths.Add(fallback);
}
}
var interestingNames = new HashSet<string>(
new[]
{
"BTSNTSvc.exe", "BTSNTSvc64.exe", "BTSMMC.msc", "Microsoft.BizTalk.ExplorerOM.dll",
"Microsoft.BizTalk.Operations.dll", "SSOConfig.exe", "ENTSSO.exe", "BREDeployment.exe"
},
StringComparer.OrdinalIgnoreCase);
foreach (var root in paths)
{
try
{
foreach (var path in Directory.EnumerateFiles(root, "*", SearchOption.AllDirectories)
.Where(path => interestingNames.Contains(Path.GetFileName(path))))
{
var info = FileVersionInfo.GetVersionInfo(path);
target.Components.Add(new NameValueRecord(
path,
(info.FileVersion ?? "[keine Dateiversion]") + " | " + (info.ProductVersion ?? "[keine Produktversion]")));
}
}
catch (Exception exception)
{
findings.Add(new Finding
{
Severity = "Warnung",
Area = "BizTalk-Komponenten",
Message = "BizTalk-Installationspfad konnte nicht vollständig gelesen werden: " + root,
TechnicalDetail = exception.GetType().Name + ": " + exception.Message,
Recommendation = "Leseberechtigungen und Installationspfad prüfen."
});
}
}
}
/// <summary>
/// Prüft zentrale BizTalk-WMI-Klassen und dokumentiert deren Instanzzahlen.
/// </summary>
/// <param name="target">BizTalk-Zielmodell.</param>
/// <param name="findings">Liste klassenspezifischer WMI-Fehler.</param>
private void ReadBizTalkWmi(BizTalkInventory target, IList<Finding> findings)
{
var scope = new ManagementScope(@"\\.\root\MicrosoftBizTalkServer");
try
{
scope.Connect();
target.WmiNamespaceAvailable = scope.IsConnected;
}
catch (Exception exception)
{
target.WmiNamespaceAvailable = false;
findings.Add(new Finding
{
Severity = "Fehler",
Area = "BizTalk WMI",
Message = "Der BizTalk-WMI-Namespace ist nicht erreichbar.",
TechnicalDetail = exception.GetType().Name + ": " + exception.Message,
Recommendation = "BizTalk-WMI-Provider, Namespace und Ausführungsberechtigungen prüfen."
});
return;
}
var classes = new[]
{
"MSBTS_GroupSetting",
"MSBTS_Host",
"MSBTS_HostInstance",
"MSBTS_ReceivePort",
"MSBTS_ReceiveLocation",
"MSBTS_SendPort",
"MSBTS_Orchestration",
"MSBTS_Server"
};
foreach (var className in classes)
{
try
{
var count = 0;
using (var searcher = new ManagementObjectSearcher(
scope,
new ObjectQuery("SELECT * FROM " + className),
new EnumerationOptions
{
ReturnImmediately = false,
Rewindable = false,
Timeout = TimeSpan.FromSeconds(options.WmiTimeoutSeconds)
}))
using (var results = searcher.Get())
{
foreach (ManagementObject ignored in results)
{
count++;
}
}
target.WmiClasses.Add(new NameValueRecord(className, count.ToString(CultureInfo.InvariantCulture) + " Instanz(en)"));
}
catch (Exception exception)
{
target.WmiClasses.Add(new NameValueRecord(className, "[Fehler: " + exception.GetType().Name + "]"));
findings.Add(new Finding
{
Severity = "Warnung",
Area = "BizTalk WMI",
Message = "WMI-Klasse konnte nicht gelesen werden: " + className,
TechnicalDetail = exception.GetType().Name + ": " + exception.Message,
Recommendation = "BizTalk-Operatorrechte, SQL-Erreichbarkeit und WMI-Provider prüfen."
});
}
}
}
}
}
@@ -0,0 +1,315 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Security.AccessControl;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using BizTalkIisEnvironmentInventory.Configuration;
using BizTalkIisEnvironmentInventory.Models;
namespace BizTalkIisEnvironmentInventory.Collectors
{
/// <summary>
/// Dokumentiert lokale Computerzertifikate und private Schlüssel ausschließlich über Metadaten.
/// </summary>
internal sealed class CertificateCollector
{
private readonly CollectorOptions options;
/// <summary>
/// Initialisiert den Zertifikat-Collector.
/// </summary>
/// <param name="options">Schalter für den Zertifikatsumfang.</param>
public CertificateCollector(CollectorOptions options)
{
this.options = options;
}
/// <summary>
/// Liest den LocalMachine-Personal-Store und markiert IIS-Bindungszertifikate.
/// </summary>
/// <param name="target">Zertifikats-Zielliste.</param>
/// <param name="iis">Bereits erfasste IIS-Bindings.</param>
/// <param name="findings">Liste für Ablauf- und Zugriffsauffälligkeiten.</param>
public void Collect(IList<CertificateRecord> target, IisInventory iis, IList<Finding> findings)
{
var bindingThumbprints = new HashSet<string>(
iis.Sites.SelectMany(site => site.Bindings)
.Select(binding => NormalizeThumbprint(binding.CertificateHash))
.Where(value => !string.IsNullOrWhiteSpace(value)),
StringComparer.OrdinalIgnoreCase);
using (var store = new X509Store(StoreName.My, StoreLocation.LocalMachine))
{
store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);
foreach (var certificate in store.Certificates.Cast<X509Certificate2>())
{
var thumbprint = NormalizeThumbprint(certificate.Thumbprint);
var usedByIis = bindingThumbprints.Contains(thumbprint);
if (!options.IncludeAllPersonalCertificates && !usedByIis)
{
continue;
}
var record = CreateRecord(certificate, usedByIis, findings);
target.Add(record);
}
}
foreach (var missing in bindingThumbprints.Where(
thumbprint => !target.Any(item => string.Equals(
NormalizeThumbprint(item.Thumbprint),
thumbprint,
StringComparison.OrdinalIgnoreCase))))
{
findings.Add(new Finding
{
Severity = "Warnung",
Area = "Zertifikate",
Message = "Das von IIS referenzierte Zertifikat wurde nicht in LocalMachine\\My gefunden.",
TechnicalDetail = "Thumbprint: " + missing,
Recommendation = "Store-Name des Bindings, Zertifikatsbereitstellung und Berechtigungen prüfen."
});
}
var sorted = target.OrderByDescending(item => item.UsedByIisBinding)
.ThenBy(item => item.Subject, StringComparer.OrdinalIgnoreCase)
.ToList();
target.Clear();
foreach (var item in sorted)
{
target.Add(item);
}
}
/// <summary>
/// Überführt ein X509-Zertifikat in ein sicheres Berichtsmodell.
/// </summary>
/// <param name="certificate">Lokales Zertifikat.</param>
/// <param name="usedByIis">Gibt an, ob ein IIS-Binding den Thumbprint verwendet.</param>
/// <param name="findings">Liste für Auffälligkeiten.</param>
/// <returns>Zertifikatsdatensatz ohne privates Schlüsselmaterial.</returns>
private static CertificateRecord CreateRecord(
X509Certificate2 certificate,
bool usedByIis,
IList<Finding> findings)
{
var record = new CertificateRecord
{
StoreLocation = StoreLocation.LocalMachine.ToString(),
StoreName = StoreName.My.ToString(),
Subject = certificate.Subject,
Issuer = certificate.Issuer,
Thumbprint = NormalizeThumbprint(certificate.Thumbprint),
SerialNumber = certificate.SerialNumber,
NotBefore = certificate.NotBefore,
NotAfter = certificate.NotAfter,
SignatureAlgorithm = certificate.SignatureAlgorithm == null
? string.Empty
: certificate.SignatureAlgorithm.FriendlyName,
PublicKeyAlgorithm = certificate.PublicKey == null
? string.Empty
: certificate.PublicKey.Oid.FriendlyName,
HasPrivateKey = certificate.HasPrivateKey,
UsedByIisBinding = usedByIis,
PrivateKeyExportable = certificate.HasPrivateKey ? "[nicht ermittelbar]" : "Nein (kein privater Schlüssel)"
};
try
{
record.PublicKeySize = certificate.PublicKey.Key.KeySize;
}
catch
{
record.PublicKeySize = 0;
}
foreach (var extension in certificate.Extensions.OfType<X509EnhancedKeyUsageExtension>())
{
foreach (var usage in extension.EnhancedKeyUsages.Cast<Oid>())
{
record.EnhancedKeyUsages.Add((usage.FriendlyName ?? "[unbekannt]") + " (" + usage.Value + ")");
}
}
if (certificate.HasPrivateKey)
{
InspectPrivateKey(certificate, record, findings);
}
if (certificate.NotAfter.ToUniversalTime() < DateTime.UtcNow)
{
AddExpiryFinding(findings, record, "Fehler", "Zertifikat ist abgelaufen.");
}
else if (certificate.NotAfter.ToUniversalTime() < DateTime.UtcNow.AddDays(60))
{
AddExpiryFinding(findings, record, "Warnung", "Zertifikat läuft innerhalb von 60 Tagen ab.");
}
return record;
}
/// <summary>
/// Liest Provider, Container, Exportpolicy und Dateiberechtigungen eines privaten Schlüssels.
/// </summary>
/// <param name="certificate">Zertifikat mit privatem Schlüssel.</param>
/// <param name="record">Zielmodell.</param>
/// <param name="findings">Liste für nicht fatale Zugriffsfehler.</param>
private static void InspectPrivateKey(
X509Certificate2 certificate,
CertificateRecord record,
IList<Finding> findings)
{
try
{
#pragma warning disable 618
using (var privateKey = certificate.PrivateKey)
#pragma warning restore 618
{
var csp = privateKey as RSACryptoServiceProvider;
if (csp != null)
{
var info = csp.CspKeyContainerInfo;
record.PrivateKeyProvider = info.ProviderName;
record.PrivateKeyContainer = info.UniqueKeyContainerName;
record.PrivateKeyExportable = info.Exportable ? "Ja" : "Nein";
record.PrivateKeyFile = LocateKeyFile(info.UniqueKeyContainerName, true);
}
else
{
var rsaCng = privateKey as RSACng;
var ecdsaCng = privateKey as ECDsaCng;
var key = rsaCng != null ? rsaCng.Key : (ecdsaCng == null ? null : ecdsaCng.Key);
if (key != null)
{
record.PrivateKeyProvider = key.Provider.Provider;
record.PrivateKeyContainer = key.UniqueName;
record.PrivateKeyExportable = FormatExportPolicy(key.ExportPolicy);
record.PrivateKeyFile = LocateKeyFile(key.UniqueName, false);
}
else
{
record.PrivateKeyProvider = privateKey == null
? "[Providerzugriff nicht möglich]"
: privateKey.GetType().FullName;
}
}
}
if (!string.IsNullOrWhiteSpace(record.PrivateKeyFile) && File.Exists(record.PrivateKeyFile))
{
CollectKeyAcl(record);
}
}
catch (Exception exception)
{
record.PrivateKeyProvider = "[nicht lesbar: " + exception.GetType().Name + "]";
findings.Add(new Finding
{
Severity = "Warnung",
Area = "Zertifikate/Private Keys",
Message = "Private-Key-Metadaten konnten nicht vollständig gelesen werden: " + record.Thumbprint,
TechnicalDetail = exception.GetType().Name + ": " + exception.Message,
Recommendation = "Tool erhöht ausführen und ACL des privaten Schlüsselcontainers prüfen."
});
}
}
/// <summary>
/// Liest die Dateisystem-ACL der Schlüsselcontainerdatei.
/// </summary>
/// <param name="record">Zertifikatsdatensatz mit Schlüsselpfad.</param>
private static void CollectKeyAcl(CertificateRecord record)
{
var security = File.GetAccessControl(record.PrivateKeyFile, AccessControlSections.Access);
var rules = security.GetAccessRules(true, true, typeof(System.Security.Principal.NTAccount));
foreach (FileSystemAccessRule rule in rules)
{
record.KeyAccessRules.Add(new AccessRuleRecord
{
Target = record.PrivateKeyFile,
Identity = rule.IdentityReference.Value,
Rights = rule.FileSystemRights.ToString(),
AccessType = rule.AccessControlType.ToString(),
IsInherited = rule.IsInherited,
Inheritance = rule.InheritanceFlags + " / " + rule.PropagationFlags
});
}
}
/// <summary>
/// Sucht die Containerdatei eines CAPI- oder CNG-Schlüssels.
/// </summary>
/// <param name="uniqueName">Eindeutiger Containername.</param>
/// <param name="capi">Gibt CAPI statt CNG an.</param>
/// <returns>Existierender Pfad oder leerer Text.</returns>
private static string LocateKeyFile(string uniqueName, bool capi)
{
if (string.IsNullOrWhiteSpace(uniqueName))
{
return string.Empty;
}
var common = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
var folder = capi
? Path.Combine(common, "Microsoft", "Crypto", "RSA", "MachineKeys")
: Path.Combine(common, "Microsoft", "Crypto", "Keys");
var path = Path.Combine(folder, uniqueName);
return File.Exists(path) ? path : string.Empty;
}
/// <summary>
/// Formatiert eine CNG-Exportpolicy ohne den Schlüssel zu exportieren.
/// </summary>
/// <param name="policy">CNG-Exportpolicy.</param>
/// <returns>Lesbarer Policytext.</returns>
private static string FormatExportPolicy(CngExportPolicies policy)
{
if (policy == CngExportPolicies.None)
{
return "Nein";
}
var exportable = (policy & CngExportPolicies.AllowExport) != 0
|| (policy & CngExportPolicies.AllowPlaintextExport) != 0;
return (exportable ? "Ja" : "Nein") + " (" + policy + ")";
}
/// <summary>
/// Fügt einen Ablaufhinweis hinzu.
/// </summary>
/// <param name="findings">Zielliste.</param>
/// <param name="record">Betroffenes Zertifikat.</param>
/// <param name="severity">Schweregrad.</param>
/// <param name="message">Aussage.</param>
private static void AddExpiryFinding(
IList<Finding> findings,
CertificateRecord record,
string severity,
string message)
{
findings.Add(new Finding
{
Severity = severity,
Area = "Zertifikate",
Message = message,
TechnicalDetail = record.Subject + " | " + record.Thumbprint + " | NotAfter "
+ record.NotAfter.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture),
Recommendation = "Erneuerung und IIS-Binding vor Ablauf terminieren."
});
}
/// <summary>
/// Normalisiert einen Thumbprint.
/// </summary>
/// <param name="value">Thumbprint.</param>
/// <returns>Großgeschriebener Wert ohne Leerzeichen.</returns>
private static string NormalizeThumbprint(string value)
{
return (value ?? string.Empty).Replace(" ", string.Empty).ToUpperInvariant();
}
}
}
@@ -0,0 +1,705 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Security.AccessControl;
using System.Security.Cryptography;
using System.Text;
using System.Xml.Linq;
using BizTalkIisEnvironmentInventory.Configuration;
using BizTalkIisEnvironmentInventory.Infrastructure;
using BizTalkIisEnvironmentInventory.Models;
namespace BizTalkIisEnvironmentInventory.Collectors
{
/// <summary>
/// Liest IIS-Konfiguration, Topologie, Webinhalte und NTFS-Berechtigungen ohne Microsoft.Web.Administration.
/// </summary>
internal sealed class IisCollector
{
private readonly CollectorOptions options;
/// <summary>
/// Initialisiert den IIS-Collector.
/// </summary>
/// <param name="options">Grenzen für Dateimanifest und Hashing.</param>
public IisCollector(CollectorOptions options)
{
this.options = options;
}
/// <summary>
/// Erfasst die lokale applicationHost.config und die daraus referenzierten Inhalte.
/// </summary>
/// <param name="target">Zielmodell für IIS-Daten.</param>
/// <param name="findings">Gemeinsame Liste nicht fataler Auffälligkeiten.</param>
/// <param name="overridePath">Optionaler Pfad für Offline-Tests oder Sonderinstallationen.</param>
public void Collect(IisInventory target, IList<Finding> findings, string overridePath)
{
var path = ResolveConfigurationPath(overridePath);
if (!File.Exists(path))
{
throw new FileNotFoundException("Die IIS-Konfiguration wurde nicht gefunden.", path);
}
target.ConfigurationPath = path;
target.ConfigurationLastWriteUtc = File.GetLastWriteTimeUtc(path);
XDocument document;
using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete))
{
document = XDocument.Load(stream, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
}
if (document.Root == null)
{
throw new InvalidDataException("Die IIS-Konfiguration besitzt kein XML-Wurzelelement.");
}
target.SanitizedConfigurationSha256 = HashSanitizedConfiguration(document.Root);
CollectEncryptionProviders(document.Root, target);
CollectSectionDeclarations(document.Root, target);
CollectApplicationPools(document.Root, target);
CollectSites(document.Root, target, findings);
ValidateExpectedApplications(target, findings);
}
/// <summary>
/// Ermittelt den IIS-Konfigurationspfad.
/// </summary>
/// <param name="overridePath">Optional explizit vorgegebener Pfad.</param>
/// <returns>Vollständiger Pfad zur applicationHost.config.</returns>
internal static string ResolveConfigurationPath(string overridePath)
{
if (!string.IsNullOrWhiteSpace(overridePath))
{
return Path.GetFullPath(overridePath);
}
var windows = Environment.GetFolderPath(Environment.SpecialFolder.Windows);
if (string.IsNullOrWhiteSpace(windows))
{
windows = Environment.ExpandEnvironmentVariables("%WINDIR%");
}
return Path.Combine(windows, "System32", "inetsrv", "config", "applicationHost.config");
}
/// <summary>
/// Erstellt einen SHA-256-Fingerprint der bereinigten Konfiguration.
/// </summary>
/// <param name="root">XML-Wurzelelement.</param>
/// <returns>Hexadezimaler SHA-256-Hash.</returns>
private static string HashSanitizedConfiguration(XElement root)
{
var safe = SensitiveDataSanitizer.SanitizeXml(root).ToString(SaveOptions.DisableFormatting);
using (var algorithm = SHA256.Create())
{
return ToHex(algorithm.ComputeHash(Encoding.UTF8.GetBytes(safe)));
}
}
/// <summary>
/// Erfasst geschützte Konfigurationsprovider, jedoch niemals deren Schlüsselmaterial.
/// </summary>
/// <param name="root">XML-Wurzelelement.</param>
/// <param name="target">Zielmodell.</param>
private static void CollectEncryptionProviders(XElement root, IisInventory target)
{
var protectedData = root.Element("configProtectedData");
var providers = protectedData == null ? null : protectedData.Element("providers");
if (providers == null)
{
return;
}
foreach (var add in providers.Elements("add"))
{
target.EncryptionProviders.Add(new EncryptionProviderRecord
{
Name = Attribute(add, "name"),
Type = Attribute(add, "type"),
KeyContainerName = Attribute(add, "keyContainerName"),
UseMachineContainer = Attribute(add, "useMachineContainer"),
Description = "Provider-Metadaten; Schlüssel und verschlüsselte Nutzdaten werden bewusst nicht exportiert."
});
}
}
/// <summary>
/// Erfasst globale IIS-Section-Deklarationen und deren grobe Belegung.
/// </summary>
/// <param name="root">XML-Wurzelelement.</param>
/// <param name="target">Zielmodell.</param>
private static void CollectSectionDeclarations(XElement root, IisInventory target)
{
var configSections = root.Element("configSections");
if (configSections == null)
{
return;
}
foreach (var section in configSections.Descendants("section"))
{
var path = BuildSectionPath(section);
var configuredElement = ResolveElementPath(root, path);
var isEncrypted = configuredElement != null
&& configuredElement.DescendantsAndSelf().Any(
element => string.Equals(element.Name.LocalName, "EncryptedData", StringComparison.OrdinalIgnoreCase));
target.GlobalSections.Add(new ConfigurationSectionRecord
{
Path = path,
OverrideModeDefault = Attribute(section, "overrideModeDefault"),
AllowDefinition = Attribute(section, "allowDefinition"),
IsEncrypted = isEncrypted,
ElementCount = configuredElement == null ? 0 : configuredElement.Descendants().Count(),
SafeSummary = BuildSafeAttributeSummary(configuredElement)
});
}
target.GlobalSections.Sort((left, right) =>
string.Compare(left.Path, right.Path, StringComparison.OrdinalIgnoreCase));
}
/// <summary>
/// Erfasst IIS-Anwendungspools und deren Identitäten.
/// </summary>
/// <param name="root">XML-Wurzelelement.</param>
/// <param name="target">Zielmodell.</param>
private static void CollectApplicationPools(XElement root, IisInventory target)
{
var host = root.Element("system.applicationHost");
var pools = host == null ? null : host.Element("applicationPools");
if (pools == null)
{
return;
}
var defaults = pools.Element("applicationPoolDefaults");
foreach (var add in pools.Elements("add"))
{
var processModel = add.Element("processModel");
var defaultProcessModel = defaults == null ? null : defaults.Element("processModel");
target.ApplicationPools.Add(new ApplicationPoolRecord
{
Name = Attribute(add, "name"),
ManagedRuntimeVersion = EffectiveAttribute(add, defaults, "managedRuntimeVersion"),
ManagedPipelineMode = EffectiveAttribute(add, defaults, "managedPipelineMode"),
AutoStart = EffectiveAttribute(add, defaults, "autoStart"),
StartMode = EffectiveAttribute(add, defaults, "startMode"),
Enable32BitAppOnWin64 = EffectiveAttribute(add, defaults, "enable32BitAppOnWin64"),
IdentityType = EffectiveAttribute(processModel, defaultProcessModel, "identityType"),
UserName = NormalizeIdentity(
EffectiveAttribute(processModel, defaultProcessModel, "identityType"),
EffectiveAttribute(processModel, defaultProcessModel, "userName"),
Attribute(add, "name"))
});
}
target.ApplicationPools.Sort((left, right) =>
string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase));
}
/// <summary>
/// Erfasst Sites, Anwendungen, virtuelle Verzeichnisse, Bindings und Inhalte.
/// </summary>
/// <param name="root">XML-Wurzelelement.</param>
/// <param name="target">Zielmodell.</param>
/// <param name="findings">Liste für nicht fatale Zugriffsfehler.</param>
private void CollectSites(XElement root, IisInventory target, IList<Finding> findings)
{
var host = root.Element("system.applicationHost");
var sites = host == null ? null : host.Element("sites");
if (sites == null)
{
throw new InvalidDataException("Der IIS-Abschnitt system.applicationHost/sites fehlt.");
}
foreach (var siteElement in sites.Elements("site"))
{
var site = new SiteRecord
{
Name = Attribute(siteElement, "name"),
Id = Attribute(siteElement, "id"),
ServerAutoStart = Attribute(siteElement, "serverAutoStart"),
LogDirectory = ExpandIisPath(Attribute(siteElement.Element("logFile"), "directory"))
};
var bindings = siteElement.Element("bindings");
if (bindings != null)
{
foreach (var binding in bindings.Elements("binding"))
{
site.Bindings.Add(new BindingRecord
{
SiteName = site.Name,
Protocol = Attribute(binding, "protocol"),
BindingInformation = Attribute(binding, "bindingInformation"),
CertificateHash = NormalizeThumbprint(Attribute(binding, "certificateHash")),
CertificateStoreName = Attribute(binding, "certificateStoreName"),
SslFlags = Attribute(binding, "sslFlags")
});
}
}
foreach (var applicationElement in siteElement.Elements("application"))
{
var application = CreateApplication(site.Name, applicationElement);
site.Applications.Add(application);
InspectApplicationContent(application, findings);
}
target.Sites.Add(site);
}
target.Sites.Sort((left, right) =>
string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase));
}
/// <summary>
/// Meldet erwartete, aber nicht konfigurierte BEW-Webanwendungen.
/// </summary>
/// <param name="target">Erfasstes IIS-Inventar.</param>
/// <param name="findings">Liste für Vollständigkeitshinweise.</param>
private void ValidateExpectedApplications(IisInventory target, IList<Finding> findings)
{
if (options.ExpectedApplications == null || options.ExpectedApplications.Count == 0)
{
return;
}
var discovered = new HashSet<string>(
target.Sites.SelectMany(site => site.Applications)
.Select(application => (application.Path ?? string.Empty).Trim().Trim('/'))
.Where(path => path.Length > 0),
StringComparer.OrdinalIgnoreCase);
foreach (var expected in options.ExpectedApplications.Where(item => !discovered.Contains(item)))
{
AddFinding(
findings,
"Warnung",
"IIS/Vollständigkeit",
"Erwartete Webanwendung wurde nicht gefunden: " + expected,
"ExpectedApplications in BizTalkIisEnvironmentInventory.exe.config",
"Prüfen, ob die Anwendung anders benannt, unter einer anderen Site konfiguriert oder in dieser Umgebung bewusst nicht vorhanden ist.");
}
}
/// <summary>
/// Erstellt ein Anwendungsmodell aus einem IIS-XML-Element.
/// </summary>
/// <param name="siteName">Name der übergeordneten Site.</param>
/// <param name="element">IIS-application-Element.</param>
/// <returns>Initialisiertes Anwendungsmodell.</returns>
private static WebApplicationRecord CreateApplication(string siteName, XElement element)
{
var result = new WebApplicationRecord
{
SiteName = siteName,
Path = Attribute(element, "path"),
ApplicationPool = Attribute(element, "applicationPool"),
EnabledProtocols = Attribute(element, "enabledProtocols")
};
foreach (var virtualDirectory in element.Elements("virtualDirectory"))
{
var physicalPath = ExpandIisPath(Attribute(virtualDirectory, "physicalPath"));
result.VirtualDirectories.Add(new VirtualDirectoryRecord
{
Path = Attribute(virtualDirectory, "path"),
PhysicalPath = physicalPath,
UserName = string.IsNullOrWhiteSpace(Attribute(virtualDirectory, "userName"))
? "[IIS-/Prozessidentität]"
: Attribute(virtualDirectory, "userName")
});
if (string.Equals(Attribute(virtualDirectory, "path"), "/", StringComparison.Ordinal))
{
result.PhysicalPath = physicalPath;
}
}
if (string.IsNullOrWhiteSpace(result.PhysicalPath) && result.VirtualDirectories.Count > 0)
{
result.PhysicalPath = result.VirtualDirectories[0].PhysicalPath;
}
return result;
}
/// <summary>
/// Inventarisiert Inhalt und ACL eines Web-Stammverzeichnisses mit Fehlerisolation.
/// </summary>
/// <param name="application">Zu untersuchende IIS-Anwendung.</param>
/// <param name="findings">Liste für nicht fatale Zugriffsfehler.</param>
private void InspectApplicationContent(WebApplicationRecord application, IList<Finding> findings)
{
if (string.IsNullOrWhiteSpace(application.PhysicalPath))
{
AddFinding(findings, "Warnung", "IIS/Webinhalt",
application.SiteName + application.Path + " besitzt keinen auflösbaren physischen Pfad.",
"physicalPath fehlt oder verwendet nicht auflösbare Variablen.",
"IIS-Konfiguration und virtuelle Verzeichnisse prüfen.");
return;
}
application.PhysicalPathExists = Directory.Exists(application.PhysicalPath);
if (!application.PhysicalPathExists)
{
AddFinding(findings, "Warnung", "IIS/Webinhalt",
"Physischer Pfad fehlt: " + application.PhysicalPath,
application.SiteName + application.Path,
"Deployment, Laufwerk/Mount und IIS-physicalPath prüfen.");
return;
}
CollectDirectoryAcl(application, findings);
if (options.MaxFilesPerApplication <= 0)
{
return;
}
var root = application.PhysicalPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
var pending = new Queue<DirectoryWorkItem>();
pending.Enqueue(new DirectoryWorkItem(root, 0));
while (pending.Count > 0)
{
var current = pending.Dequeue();
try
{
foreach (var filePath in Directory.EnumerateFiles(current.Path))
{
InspectFile(application, root, filePath, findings);
}
if (current.Depth >= options.MaxContentDepth)
{
application.ManifestTruncated = true;
continue;
}
foreach (var directoryPath in Directory.EnumerateDirectories(current.Path))
{
var attributes = File.GetAttributes(directoryPath);
if ((attributes & FileAttributes.ReparsePoint) != 0)
{
AddFinding(findings, "Hinweis", "IIS/Webinhalt",
"Reparse Point wurde nicht rekursiv verfolgt: " + directoryPath,
"Schutz vor Schleifen und Verlassen des Web-Stammverzeichnisses.",
"Verknüpftes Ziel bei Migrationsbedarf separat dokumentieren.");
continue;
}
pending.Enqueue(new DirectoryWorkItem(directoryPath, current.Depth + 1));
}
}
catch (Exception exception)
{
application.ManifestTruncated = true;
AddFinding(findings, "Warnung", "IIS/Webinhalt",
"Verzeichnis konnte nicht vollständig gelesen werden: " + current.Path,
exception.GetType().Name + ": " + exception.Message,
"Collector erhöht ausführen oder ACL gezielt prüfen.");
}
}
}
/// <summary>
/// Erfasst Metadaten und optional SHA-256 eines einzelnen Webinhalts.
/// </summary>
/// <param name="application">Zielanwendung.</param>
/// <param name="root">Normalisierter Web-Stammpfad.</param>
/// <param name="filePath">Vollständiger Dateipfad.</param>
/// <param name="findings">Liste nicht fataler Auffälligkeiten.</param>
private void InspectFile(
WebApplicationRecord application,
string root,
string filePath,
IList<Finding> findings)
{
try
{
var info = new FileInfo(filePath);
application.TotalFiles++;
application.TotalBytes += info.Length;
if (!application.LatestWriteUtc.HasValue || info.LastWriteTimeUtc > application.LatestWriteUtc.Value)
{
application.LatestWriteUtc = info.LastWriteTimeUtc;
}
if (application.ContentFiles.Count >= options.MaxFilesPerApplication)
{
application.ManifestTruncated = true;
return;
}
application.ContentFiles.Add(new ContentFileRecord
{
RelativePath = filePath.Length > root.Length
? filePath.Substring(root.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
: info.Name,
SizeBytes = info.Length,
LastWriteUtc = info.LastWriteTimeUtc,
Sha256 = options.IncludeFileHashes ? HashFile(filePath) : string.Empty
});
}
catch (Exception exception)
{
application.ManifestTruncated = true;
AddFinding(findings, "Warnung", "IIS/Webinhalt",
"Datei konnte nicht inventarisiert werden: " + filePath,
exception.GetType().Name + ": " + exception.Message,
"Dateisperre und Leseberechtigung prüfen.");
}
}
/// <summary>
/// Liest die expliziten und geerbten ACL-Regeln eines Web-Stammverzeichnisses.
/// </summary>
/// <param name="application">Zielanwendung.</param>
/// <param name="findings">Liste nicht fataler Auffälligkeiten.</param>
private static void CollectDirectoryAcl(WebApplicationRecord application, IList<Finding> findings)
{
try
{
var security = Directory.GetAccessControl(
application.PhysicalPath,
AccessControlSections.Access | AccessControlSections.Owner);
var rules = security.GetAccessRules(true, true, typeof(System.Security.Principal.NTAccount));
foreach (FileSystemAccessRule rule in rules)
{
application.AccessRules.Add(new AccessRuleRecord
{
Target = application.PhysicalPath,
Identity = rule.IdentityReference.Value,
Rights = rule.FileSystemRights.ToString(),
AccessType = rule.AccessControlType.ToString(),
IsInherited = rule.IsInherited,
Inheritance = rule.InheritanceFlags + " / " + rule.PropagationFlags
});
}
}
catch (Exception exception)
{
AddFinding(findings, "Warnung", "NTFS",
"ACL konnte nicht gelesen werden: " + application.PhysicalPath,
exception.GetType().Name + ": " + exception.Message,
"Mit erhöhten Leserechten erneut ausführen.");
}
}
/// <summary>
/// Berechnet SHA-256 einer Datei mit geteilter Lesefreigabe.
/// </summary>
/// <param name="path">Vollständiger Dateipfad.</param>
/// <returns>Hexadezimaler SHA-256-Hash.</returns>
private static string HashFile(string path)
{
using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete))
using (var algorithm = SHA256.Create())
{
return ToHex(algorithm.ComputeHash(stream));
}
}
/// <summary>
/// Baut den vollständigen Pfad einer Section-Deklaration auf.
/// </summary>
/// <param name="section">Section-XML-Element.</param>
/// <returns>Pfad mit Slash-Trennung.</returns>
private static string BuildSectionPath(XElement section)
{
var names = new Stack<string>();
names.Push(Attribute(section, "name"));
var parent = section.Parent;
while (parent != null && string.Equals(parent.Name.LocalName, "sectionGroup", StringComparison.Ordinal))
{
names.Push(Attribute(parent, "name"));
parent = parent.Parent;
}
return string.Join("/", names.ToArray());
}
/// <summary>
/// Löst einen Section-Pfad gegen die Konfigurationswurzel auf.
/// </summary>
/// <param name="root">Konfigurationswurzel.</param>
/// <param name="path">Slash-getrennter Section-Pfad.</param>
/// <returns>Gefundenes Element oder <c>null</c>.</returns>
private static XElement ResolveElementPath(XElement root, string path)
{
var current = root;
foreach (var part in path.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries))
{
current = current == null ? null : current.Element(part);
}
return current;
}
/// <summary>
/// Erstellt eine begrenzte, secret-bereinigte Attributübersicht.
/// </summary>
/// <param name="element">Konfigurationselement.</param>
/// <returns>Kurze Attributübersicht.</returns>
private static string BuildSafeAttributeSummary(XElement element)
{
if (element == null)
{
return "[nicht global konfiguriert]";
}
return string.Join(
"; ",
element.Attributes()
.Take(20)
.Select(attribute =>
attribute.Name.LocalName + "="
+ (SensitiveDataSanitizer.IsSensitiveName(attribute.Name.LocalName)
? "[ENTFERNT]"
: Limit(attribute.Value, 160))));
}
/// <summary>
/// Liest einen Attributwert nullsicher.
/// </summary>
/// <param name="element">XML-Element oder <c>null</c>.</param>
/// <param name="name">Attributname.</param>
/// <returns>Attributwert oder leerer Text.</returns>
private static string Attribute(XElement element, string name)
{
var attribute = element == null ? null : element.Attribute(name);
return attribute == null ? string.Empty : attribute.Value;
}
/// <summary>
/// Liest einen lokalen Attributwert oder den Wert des Defaults.
/// </summary>
/// <param name="element">Lokales XML-Element.</param>
/// <param name="defaults">Default-XML-Element.</param>
/// <param name="name">Attributname.</param>
/// <returns>Effektiver Wert oder leerer Text.</returns>
private static string EffectiveAttribute(XElement element, XElement defaults, string name)
{
var local = Attribute(element, name);
return string.IsNullOrWhiteSpace(local) ? Attribute(defaults, name) : local;
}
/// <summary>
/// Stellt eine verständliche Anwendungspool-Identität her.
/// </summary>
/// <param name="identityType">IIS-Identitätstyp.</param>
/// <param name="userName">Optionales Custom-Konto.</param>
/// <param name="poolName">Name des Anwendungspools.</param>
/// <returns>Effektiv zu erwartende Identität.</returns>
private static string NormalizeIdentity(string identityType, string userName, string poolName)
{
if (!string.IsNullOrWhiteSpace(userName))
{
return userName;
}
if (string.Equals(identityType, "ApplicationPoolIdentity", StringComparison.OrdinalIgnoreCase)
|| string.IsNullOrWhiteSpace(identityType))
{
return @"IIS APPPOOL\" + poolName;
}
return identityType;
}
/// <summary>
/// Expandiert Umgebungsvariablen und normalisiert IIS-Pfade.
/// </summary>
/// <param name="path">IIS-Pfad.</param>
/// <returns>Expandierter Pfad.</returns>
private static string ExpandIisPath(string path)
{
return string.IsNullOrWhiteSpace(path)
? string.Empty
: Environment.ExpandEnvironmentVariables(path.Trim());
}
/// <summary>
/// Normalisiert einen Zertifikat-Fingerprint.
/// </summary>
/// <param name="value">Fingerprint aus IIS.</param>
/// <returns>Großgeschriebener Fingerprint ohne Leerzeichen.</returns>
private static string NormalizeThumbprint(string value)
{
return (value ?? string.Empty).Replace(" ", string.Empty).ToUpperInvariant();
}
/// <summary>
/// Begrenzt sehr lange Berichtswerte.
/// </summary>
/// <param name="value">Eingabewert.</param>
/// <param name="maximum">Maximale Zeichenzahl.</param>
/// <returns>Original oder gekürzter Wert.</returns>
private static string Limit(string value, int maximum)
{
return value != null && value.Length > maximum
? value.Substring(0, maximum) + "…"
: value ?? string.Empty;
}
/// <summary>
/// Wandelt Bytes in einen Hexadezimaltext um.
/// </summary>
/// <param name="bytes">Eingabebytes.</param>
/// <returns>Großgeschriebener Hexadezimaltext.</returns>
private static string ToHex(byte[] bytes)
{
return BitConverter.ToString(bytes).Replace("-", string.Empty);
}
/// <summary>
/// Fügt ein Finding standardisiert hinzu.
/// </summary>
/// <param name="findings">Zielliste.</param>
/// <param name="severity">Schweregrad.</param>
/// <param name="area">Fachlicher Bereich.</param>
/// <param name="message">Benutzerlesbare Aussage.</param>
/// <param name="detail">Technisches Detail.</param>
/// <param name="recommendation">Empfohlene Folgemaßnahme.</param>
private static void AddFinding(
IList<Finding> findings,
string severity,
string area,
string message,
string detail,
string recommendation)
{
findings.Add(new Finding
{
Severity = severity,
Area = area,
Message = message,
TechnicalDetail = detail,
Recommendation = recommendation
});
}
private sealed class DirectoryWorkItem
{
/// <summary>
/// Initialisiert einen Eintrag für die iterative Verzeichnisbegehung.
/// </summary>
/// <param name="path">Zu lesender Verzeichnispfad.</param>
/// <param name="depth">Tiefe relativ zum Web-Stamm.</param>
public DirectoryWorkItem(string path, int depth)
{
Path = path;
Depth = depth;
}
public string Path { get; private set; }
public int Depth { get; private set; }
}
}
}
@@ -0,0 +1,262 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Management;
using System.Text;
using BizTalkIisEnvironmentInventory.Configuration;
using BizTalkIisEnvironmentInventory.Models;
namespace BizTalkIisEnvironmentInventory.Collectors
{
/// <summary>
/// Erfasst relevante Windows-Dienste, Dienstkonten und lokale Sicherheitsrichtlinien.
/// </summary>
internal sealed class SecurityCollector
{
private readonly CollectorOptions options;
/// <summary>
/// Initialisiert den Security-Collector.
/// </summary>
/// <param name="options">WMI-Timeout und technische Grenzen.</param>
public SecurityCollector(CollectorOptions options)
{
this.options = options;
}
/// <summary>
/// Erfasst IIS-/BizTalk-Dienstidentitäten und einen lesbaren secedit-Snapshot.
/// </summary>
/// <param name="target">Security-Zielmodell.</param>
/// <param name="iis">IIS-Daten für Anwendungspoolkonten.</param>
/// <param name="findings">Liste nicht fataler Auffälligkeiten.</param>
public void Collect(SecurityInventory target, IisInventory iis, IList<Finding> findings)
{
foreach (var pool in iis.ApplicationPools)
{
target.ServiceAccounts.Add(new ServiceAccountRecord
{
Source = "IIS Application Pool",
Name = pool.Name,
DisplayName = pool.Name,
Account = pool.UserName,
State = "[Laufzeitstatus nicht aus applicationHost.config verfügbar]",
StartMode = pool.StartMode,
Path = string.Empty
});
}
CollectWindowsServices(target);
CollectLocalSecurityPolicy(target, findings);
}
/// <summary>
/// Liest relevante Windows-Dienste per WMI.
/// </summary>
/// <param name="target">Security-Zielmodell.</param>
private void CollectWindowsServices(SecurityInventory target)
{
var scope = new ManagementScope(@"\\.\root\cimv2");
scope.Connect();
var query = "SELECT Name, DisplayName, StartName, State, StartMode, PathName FROM Win32_Service";
using (var searcher = new ManagementObjectSearcher(
scope,
new ObjectQuery(query),
new EnumerationOptions
{
ReturnImmediately = false,
Rewindable = false,
Timeout = TimeSpan.FromSeconds(options.WmiTimeoutSeconds)
}))
using (var results = searcher.Get())
{
foreach (ManagementObject service in results)
{
var name = Value(service, "Name");
var displayName = Value(service, "DisplayName");
if (!IsRelevantService(name, displayName))
{
continue;
}
target.ServiceAccounts.Add(new ServiceAccountRecord
{
Source = "Windows Service",
Name = name,
DisplayName = displayName,
Account = Value(service, "StartName"),
State = Value(service, "State"),
StartMode = Value(service, "StartMode"),
Path = Value(service, "PathName")
});
}
}
target.ServiceAccounts.Sort((left, right) =>
string.Compare(left.Source + left.Name, right.Source + right.Name, StringComparison.OrdinalIgnoreCase));
}
/// <summary>
/// Exportiert lokale User Rights und System Access über das Windows-Bordmittel secedit.
/// </summary>
/// <param name="target">Security-Zielmodell.</param>
/// <param name="findings">Liste für Berechtigungs- oder Toolfehler.</param>
private static void CollectLocalSecurityPolicy(SecurityInventory target, IList<Finding> findings)
{
var temporaryPath = Path.Combine(
Path.GetTempPath(),
"BizTalkIisInventory-" + Guid.NewGuid().ToString("N") + ".inf");
try
{
var windows = Environment.GetFolderPath(Environment.SpecialFolder.Windows);
var executable = Path.Combine(windows, "System32", "secedit.exe");
if (!File.Exists(executable))
{
throw new FileNotFoundException("secedit.exe wurde nicht gefunden.", executable);
}
var startInfo = new ProcessStartInfo
{
FileName = executable,
Arguments = "/export /cfg \"" + temporaryPath + "\" /areas USER_RIGHTS SECURITYPOLICY /quiet",
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
WorkingDirectory = Path.GetTempPath()
};
using (var process = Process.Start(startInfo))
{
if (process == null)
{
throw new InvalidOperationException("secedit.exe konnte nicht gestartet werden.");
}
if (!process.WaitForExit(60000))
{
try
{
process.Kill();
}
catch
{
// Der Report enthält bereits den Timeout; Kill-Fehler ist sekundär.
}
throw new TimeoutException("secedit.exe wurde nach 60 Sekunden beendet.");
}
var error = process.StandardError.ReadToEnd();
if (process.ExitCode != 0 || !File.Exists(temporaryPath))
{
throw new InvalidOperationException(
"secedit.exe ExitCode " + process.ExitCode + ": " + error.Trim());
}
}
ParseSecurityPolicy(temporaryPath, target);
}
catch (Exception exception)
{
findings.Add(new Finding
{
Severity = "Warnung",
Area = "Lokale Sicherheitsrichtlinie",
Message = "Die lokale Sicherheitsrichtlinie konnte nicht vollständig exportiert werden.",
TechnicalDetail = exception.GetType().Name + ": " + exception.Message,
Recommendation = "Tool als lokaler Administrator starten und secedit-Verfügbarkeit prüfen."
});
}
finally
{
try
{
if (File.Exists(temporaryPath))
{
File.Delete(temporaryPath);
}
}
catch
{
// Temporäre Datei enthält nur Richtlinienmetadaten; Cleanup-Fehler darf Report nicht verhindern.
}
}
}
/// <summary>
/// Parst die relevanten Abschnitte eines secedit-Exports.
/// </summary>
/// <param name="path">Pfad zur temporären INF-Datei.</param>
/// <param name="target">Security-Zielmodell.</param>
internal static void ParseSecurityPolicy(string path, SecurityInventory target)
{
var section = string.Empty;
foreach (var rawLine in File.ReadAllLines(path, Encoding.Unicode))
{
var line = rawLine.Trim();
if (line.Length == 0 || line.StartsWith(";", StringComparison.Ordinal))
{
continue;
}
if (line.StartsWith("[", StringComparison.Ordinal) && line.EndsWith("]", StringComparison.Ordinal))
{
section = line.Substring(1, line.Length - 2);
continue;
}
var separator = line.IndexOf('=');
if (separator < 0)
{
continue;
}
var name = line.Substring(0, separator).Trim();
var value = line.Substring(separator + 1).Trim();
if (string.Equals(section, "Privilege Rights", StringComparison.OrdinalIgnoreCase))
{
target.UserRights.Add(new UserRightRecord { Right = name, Accounts = value });
}
else if (string.Equals(section, "System Access", StringComparison.OrdinalIgnoreCase)
|| string.Equals(section, "Event Audit", StringComparison.OrdinalIgnoreCase))
{
target.LocalPolicy.Add(new NameValueRecord(section + " / " + name, value));
}
}
}
/// <summary>
/// Entscheidet, ob ein Dienst IIS-, BizTalk- oder SSO-relevant ist.
/// </summary>
/// <param name="name">Technischer Dienstname.</param>
/// <param name="displayName">Anzeigename.</param>
/// <returns><c>true</c> für relevante Dienste.</returns>
private static bool IsRelevantService(string name, string displayName)
{
var value = (name ?? string.Empty) + " " + (displayName ?? string.Empty);
var fragments = new[]
{
"BizTalk", "BTSSvc", "ENTSSO", "RuleEngine", "W3SVC", "WAS",
"IISADMIN", "AppHostSvc", "MSMQ", "World Wide Web"
};
return fragments.Any(fragment =>
value.IndexOf(fragment, StringComparison.OrdinalIgnoreCase) >= 0);
}
/// <summary>
/// Liest eine WMI-Property als Text.
/// </summary>
/// <param name="item">WMI-Objekt.</param>
/// <param name="property">Propertyname.</param>
/// <returns>Invariant formatierter Wert.</returns>
private static string Value(ManagementObject item, string property)
{
return Convert.ToString(item[property], CultureInfo.InvariantCulture);
}
}
}
@@ -0,0 +1,219 @@
using System;
using System.Globalization;
using System.Management;
using System.Runtime.InteropServices;
using System.Security.Principal;
using Microsoft.Win32;
using BizTalkIisEnvironmentInventory.Configuration;
using BizTalkIisEnvironmentInventory.Models;
namespace BizTalkIisEnvironmentInventory.Collectors
{
/// <summary>
/// Erfasst Betriebssystem, Prozesskontext und installierte IIS-nahe Windows-Rollen.
/// </summary>
internal sealed class SystemCollector
{
private readonly CollectorOptions options;
/// <summary>
/// Initialisiert den System-Collector.
/// </summary>
/// <param name="options">Collector-Grenzen und Timeouts.</param>
public SystemCollector(CollectorOptions options)
{
this.options = options;
}
/// <summary>
/// Liest lokale Systeminformationen und relevante Server Features.
/// </summary>
/// <param name="target">Zielmodell für die Ergebnisse.</param>
public void Collect(SystemInventory target)
{
target.Properties.Add(new NameValueRecord("Computername", Environment.MachineName));
target.Properties.Add(new NameValueRecord("Domäne", Environment.UserDomainName));
target.Properties.Add(new NameValueRecord("Ausführungsidentität", GetIdentity()));
target.Properties.Add(new NameValueRecord("64-Bit-Betriebssystem", Environment.Is64BitOperatingSystem.ToString()));
target.Properties.Add(new NameValueRecord("64-Bit-Prozess", Environment.Is64BitProcess.ToString()));
target.Properties.Add(new NameValueRecord(".NET-Laufzeit", RuntimeEnvironment.GetSystemVersion()));
target.Properties.Add(new NameValueRecord("Zeitzone", TimeZoneInfo.Local.DisplayName));
target.Properties.Add(new NameValueRecord("Erfassungszeit lokal", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss zzz", CultureInfo.InvariantCulture)));
ReadOperatingSystem(target);
ReadDotNetRelease(target);
ReadServerFeatures(target);
}
/// <summary>
/// Ermittelt die aktuelle Windows-Identität.
/// </summary>
/// <returns>Kontoname oder ein erklärender Fallback.</returns>
private static string GetIdentity()
{
try
{
using (var identity = WindowsIdentity.GetCurrent())
{
return identity == null ? "[nicht ermittelbar]" : identity.Name;
}
}
catch (Exception exception)
{
return "[nicht ermittelbar: " + exception.GetType().Name + "]";
}
}
/// <summary>
/// Liest Betriebssystemdetails per WMI.
/// </summary>
/// <param name="target">Zielmodell.</param>
private void ReadOperatingSystem(SystemInventory target)
{
var scope = new ManagementScope(@"\\.\root\cimv2");
scope.Connect();
using (var searcher = CreateSearcher(
scope,
"SELECT Caption, Version, BuildNumber, OSArchitecture, InstallDate, LastBootUpTime FROM Win32_OperatingSystem"))
using (var results = searcher.Get())
{
foreach (ManagementObject item in results)
{
AddWmiValue(target, "Betriebssystem", item, "Caption");
AddWmiValue(target, "Version", item, "Version");
AddWmiValue(target, "Build", item, "BuildNumber");
AddWmiValue(target, "Architektur", item, "OSArchitecture");
AddWmiDate(target, "Installationsdatum", item, "InstallDate");
AddWmiDate(target, "Letzter Start", item, "LastBootUpTime");
break;
}
}
}
/// <summary>
/// Liest den .NET-Framework-Releasewert aus der Registry.
/// </summary>
/// <param name="target">Zielmodell.</param>
private static void ReadDotNetRelease(SystemInventory target)
{
using (var key = Registry.LocalMachine.OpenSubKey(
@"SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full",
false))
{
var release = key == null ? null : key.GetValue("Release");
target.Properties.Add(new NameValueRecord(
".NET Framework Release",
release == null ? "[nicht gefunden]" : Convert.ToString(release, CultureInfo.InvariantCulture)));
}
}
/// <summary>
/// Liest installierte Serverrollen über Win32_ServerFeature.
/// </summary>
/// <param name="target">Zielmodell.</param>
private void ReadServerFeatures(SystemInventory target)
{
var scope = new ManagementScope(@"\\.\root\cimv2");
scope.Connect();
using (var searcher = CreateSearcher(
scope,
"SELECT ID, Name, ParentID FROM Win32_ServerFeature"))
using (var results = searcher.Get())
{
foreach (ManagementObject item in results)
{
var name = Convert.ToString(item["Name"], CultureInfo.InvariantCulture);
if (!IsRelevantFeature(name))
{
continue;
}
var id = Convert.ToString(item["ID"], CultureInfo.InvariantCulture);
var parent = Convert.ToString(item["ParentID"], CultureInfo.InvariantCulture);
target.InstalledFeatures.Add(new NameValueRecord(
name,
string.Format(CultureInfo.InvariantCulture, "Installiert (ID {0}, Parent {1})", id, parent)));
}
}
target.InstalledFeatures.Sort((left, right) =>
string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase));
}
/// <summary>
/// Erstellt einen WMI-Searcher mit begrenztem Timeout.
/// </summary>
/// <param name="scope">Bereits verbundener WMI-Scope.</param>
/// <param name="query">Schreibgeschützte WQL-Abfrage.</param>
/// <returns>Konfigurierter Searcher.</returns>
private ManagementObjectSearcher CreateSearcher(ManagementScope scope, string query)
{
return new ManagementObjectSearcher(
scope,
new ObjectQuery(query),
new EnumerationOptions
{
ReturnImmediately = false,
Rewindable = false,
Timeout = TimeSpan.FromSeconds(options.WmiTimeoutSeconds)
});
}
/// <summary>
/// Entscheidet, ob ein Feature für IIS/BizTalk-Dokumentation relevant ist.
/// </summary>
/// <param name="name">Anzeigename des Server Features.</param>
/// <returns><c>true</c> bei IIS-, WAS-, HTTP-, MSMQ-, .NET- oder COM+-Bezug.</returns>
private static bool IsRelevantFeature(string name)
{
var value = name ?? string.Empty;
var fragments = new[] { "IIS", "Web", "HTTP", "WAS", "ASP.NET", ".NET", "MSMQ", "Message Queuing", "COM+" };
foreach (var fragment in fragments)
{
if (value.IndexOf(fragment, StringComparison.OrdinalIgnoreCase) >= 0)
{
return true;
}
}
return false;
}
/// <summary>
/// Fügt eine WMI-Property als Text hinzu.
/// </summary>
/// <param name="target">Zielmodell.</param>
/// <param name="label">Berichtsbezeichnung.</param>
/// <param name="item">WMI-Objekt.</param>
/// <param name="property">Propertyname.</param>
private static void AddWmiValue(SystemInventory target, string label, ManagementObject item, string property)
{
target.Properties.Add(new NameValueRecord(
label,
Convert.ToString(item[property], CultureInfo.InvariantCulture)));
}
/// <summary>
/// Fügt einen WMI-DMTF-Zeitwert formatiert hinzu.
/// </summary>
/// <param name="target">Zielmodell.</param>
/// <param name="label">Berichtsbezeichnung.</param>
/// <param name="item">WMI-Objekt.</param>
/// <param name="property">Propertyname.</param>
private static void AddWmiDate(SystemInventory target, string label, ManagementObject item, string property)
{
var raw = Convert.ToString(item[property], CultureInfo.InvariantCulture);
try
{
target.Properties.Add(new NameValueRecord(
label,
ManagementDateTimeConverter.ToDateTime(raw).ToString("yyyy-MM-dd HH:mm:ss zzz", CultureInfo.InvariantCulture)));
}
catch
{
target.Properties.Add(new NameValueRecord(label, raw));
}
}
}
}
@@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
namespace BizTalkIisEnvironmentInventory.Configuration
{
/// <summary>
/// Enthält die technischen Grenzen und Schalter der Bestandsaufnahme.
/// </summary>
internal sealed class CollectorOptions
{
public int MaxFilesPerApplication { get; private set; }
public int MaxContentDepth { get; private set; }
public bool IncludeFileHashes { get; private set; }
public bool IncludeAllPersonalCertificates { get; private set; }
public int WmiTimeoutSeconds { get; private set; }
public IList<string> ExpectedApplications { get; private set; }
/// <summary>
/// Liest die Einstellungen aus der Anwendungskonfiguration und verwendet bei ungültigen Werten sichere Defaults.
/// </summary>
/// <param name="commandLine">Optionale Überschreibungen aus der Kommandozeile.</param>
/// <returns>Validierte Collector-Einstellungen.</returns>
public static CollectorOptions Load(Infrastructure.CommandLineOptions commandLine)
{
var result = new CollectorOptions
{
MaxFilesPerApplication = ReadInt("MaxFilesPerApplication", 10000, 0, 100000),
MaxContentDepth = ReadInt("MaxContentDepth", 30, 1, 100),
IncludeFileHashes = ReadBool("IncludeFileHashes", false),
IncludeAllPersonalCertificates = ReadBool("IncludeAllPersonalCertificates", true),
WmiTimeoutSeconds = ReadInt("WmiTimeoutSeconds", 30, 5, 300),
ExpectedApplications = (ConfigurationManager.AppSettings["ExpectedApplications"] ?? string.Empty)
.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(item => item.Trim().Trim('/'))
.Where(item => item.Length > 0)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList()
};
if (commandLine.IncludeFileHashes)
{
result.IncludeFileHashes = true;
}
if (commandLine.SkipContentManifest)
{
result.MaxFilesPerApplication = 0;
}
return result;
}
/// <summary>
/// Liest einen begrenzten Ganzzahlwert aus appSettings.
/// </summary>
/// <param name="key">Name der Einstellung.</param>
/// <param name="fallback">Fallback bei fehlendem oder ungültigem Wert.</param>
/// <param name="minimum">Kleinster zulässiger Wert.</param>
/// <param name="maximum">Größter zulässiger Wert.</param>
/// <returns>Validierter Wert.</returns>
private static int ReadInt(string key, int fallback, int minimum, int maximum)
{
int value;
if (!int.TryParse(ConfigurationManager.AppSettings[key], out value))
{
return fallback;
}
return Math.Max(minimum, Math.Min(maximum, value));
}
/// <summary>
/// Liest einen booleschen Wert aus appSettings.
/// </summary>
/// <param name="key">Name der Einstellung.</param>
/// <param name="fallback">Fallback bei fehlendem oder ungültigem Wert.</param>
/// <returns>Gelesener oder vorgegebener Wert.</returns>
private static bool ReadBool(string key, bool fallback)
{
bool value;
return bool.TryParse(ConfigurationManager.AppSettings[key], out value) ? value : fallback;
}
}
}
@@ -0,0 +1,111 @@
using System;
using System.IO;
using System.Text.RegularExpressions;
namespace BizTalkIisEnvironmentInventory.Infrastructure
{
/// <summary>
/// Beschreibt die validierten Kommandozeilenoptionen.
/// </summary>
internal sealed class CommandLineOptions
{
public string EnvironmentName { get; private set; }
public string OutputDirectory { get; private set; }
public string IisConfigPath { get; private set; }
public bool IncludeFileHashes { get; private set; }
public bool SkipContentManifest { get; private set; }
public bool ShowHelp { get; private set; }
public bool SelfTest { get; private set; }
/// <summary>
/// Analysiert und validiert die Kommandozeile.
/// </summary>
/// <param name="args">Argumente des Prozesses.</param>
/// <returns>Validierte Optionen.</returns>
/// <exception cref="ArgumentException">Wird bei unbekannten oder unvollständigen Optionen ausgelöst.</exception>
public static CommandLineOptions Parse(string[] args)
{
var result = new CommandLineOptions
{
EnvironmentName = "UNBEKANNT",
OutputDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "reports")
};
for (var index = 0; index < args.Length; index++)
{
switch (args[index].ToLowerInvariant())
{
case "--environment":
case "-e":
result.EnvironmentName = RequireValue(args, ref index);
break;
case "--output":
case "-o":
result.OutputDirectory = RequireValue(args, ref index);
break;
case "--iis-config":
result.IisConfigPath = RequireValue(args, ref index);
break;
case "--include-file-hashes":
result.IncludeFileHashes = true;
break;
case "--skip-content-manifest":
result.SkipContentManifest = true;
break;
case "--self-test":
result.SelfTest = true;
break;
case "--help":
case "-h":
case "/?":
result.ShowHelp = true;
break;
default:
throw new ArgumentException("Unbekannte Option: " + args[index]);
}
}
result.EnvironmentName = SanitizeEnvironment(result.EnvironmentName);
result.OutputDirectory = Path.GetFullPath(
Environment.ExpandEnvironmentVariables(result.OutputDirectory));
if (!string.IsNullOrWhiteSpace(result.IisConfigPath))
{
result.IisConfigPath = Path.GetFullPath(
Environment.ExpandEnvironmentVariables(result.IisConfigPath));
}
return result;
}
/// <summary>
/// Normalisiert einen Umgebungsnamen für Dateiname und Bericht.
/// </summary>
/// <param name="value">Eingegebener Umgebungsname.</param>
/// <returns>Sicherer Umgebungsname.</returns>
internal static string SanitizeEnvironment(string value)
{
var trimmed = string.IsNullOrWhiteSpace(value) ? "UNBEKANNT" : value.Trim();
var sanitized = Regex.Replace(trimmed, @"[^A-Za-z0-9._-]+", "-").Trim('-', '.');
return string.IsNullOrEmpty(sanitized) ? "UNBEKANNT" : sanitized.ToUpperInvariant();
}
/// <summary>
/// Liest den Wert hinter einer Option.
/// </summary>
/// <param name="args">Alle Prozessargumente.</param>
/// <param name="index">Aktueller Argumentindex; wird auf den Wert weitergeschaltet.</param>
/// <returns>Optionswert.</returns>
private static string RequireValue(string[] args, ref int index)
{
if (index + 1 >= args.Length || args[index + 1].StartsWith("-", StringComparison.Ordinal))
{
throw new ArgumentException("Wert fehlt hinter " + args[index] + ".");
}
index++;
return args[index];
}
}
}
@@ -0,0 +1,107 @@
using System;
using System.Globalization;
using System.IO;
using System.Text;
namespace BizTalkIisEnvironmentInventory.Infrastructure
{
/// <summary>
/// Schreibt sichtbare Konsolenmeldungen und ein dauerhaftes UTF-8-Protokoll.
/// </summary>
internal sealed class FileLogger : IDisposable
{
private readonly object sync = new object();
private readonly StreamWriter writer;
/// <summary>
/// Initialisiert das Protokoll.
/// </summary>
/// <param name="logPath">Vollständiger Pfad zur Protokolldatei.</param>
public FileLogger(string logPath)
{
Directory.CreateDirectory(Path.GetDirectoryName(logPath));
writer = new StreamWriter(logPath, false, new UTF8Encoding(false)) { AutoFlush = true };
LogPath = logPath;
}
public string LogPath { get; private set; }
/// <summary>
/// Schreibt eine Informationsmeldung.
/// </summary>
/// <param name="message">Benutzerlesbarer Meldungstext.</param>
public void Info(string message)
{
Write("INFO", message, ConsoleColor.Gray);
}
/// <summary>
/// Schreibt eine Warnung.
/// </summary>
/// <param name="message">Benutzerlesbarer Meldungstext.</param>
public void Warning(string message)
{
Write("WARN", message, ConsoleColor.Yellow);
}
/// <summary>
/// Schreibt einen Fehler einschließlich technischer Details.
/// </summary>
/// <param name="message">Benutzerlesbarer Meldungstext.</param>
/// <param name="exception">Optionale Ausnahme.</param>
public void Error(string message, Exception exception = null)
{
var detail = exception == null ? message : message + " | " + exception;
Write("ERROR", detail, ConsoleColor.Red);
}
/// <summary>
/// Schreibt einen erfolgreichen Arbeitsschritt.
/// </summary>
/// <param name="message">Benutzerlesbarer Meldungstext.</param>
public void Success(string message)
{
Write("OK", message, ConsoleColor.Green);
}
/// <summary>
/// Gibt den Dateihandle des Protokolls frei.
/// </summary>
public void Dispose()
{
writer.Dispose();
}
/// <summary>
/// Schreibt atomar auf Konsole und in Datei.
/// </summary>
/// <param name="level">Kurzbezeichnung der Meldungsstufe.</param>
/// <param name="message">Meldung.</param>
/// <param name="color">Konsolenfarbe.</param>
private void Write(string level, string message, ConsoleColor color)
{
var line = string.Format(
CultureInfo.InvariantCulture,
"{0:yyyy-MM-dd HH:mm:ss.fff} [{1}] {2}",
DateTime.Now,
level,
message);
lock (sync)
{
writer.WriteLine(line);
var previous = Console.ForegroundColor;
try
{
Console.ForegroundColor = color;
Console.WriteLine(line);
}
finally
{
Console.ForegroundColor = previous;
}
}
}
}
}
@@ -0,0 +1,72 @@
using System;
using System.Diagnostics;
using BizTalkIisEnvironmentInventory.Models;
namespace BizTalkIisEnvironmentInventory.Infrastructure
{
/// <summary>
/// Isoliert Collector-Fehler und überführt sie in Status und Report-Finding.
/// </summary>
internal static class SafeCollector
{
/// <summary>
/// Führt einen Collector aus, protokolliert Laufzeit und fängt nicht behandelte Fehler ab.
/// </summary>
/// <param name="document">Zieldokument für Status und Findings.</param>
/// <param name="logger">Gemeinsames Laufprotokoll.</param>
/// <param name="step">Aktueller Schritt beginnend bei eins.</param>
/// <param name="totalSteps">Gesamtzahl der Schritte.</param>
/// <param name="name">Benutzerlesbarer Abschnittsname.</param>
/// <param name="action">Auszuführender Collector.</param>
internal static void Run(
InventoryDocument document,
FileLogger logger,
int step,
int totalSteps,
string name,
Action action)
{
var stopwatch = Stopwatch.StartNew();
logger.Info(string.Format("[{0}/{1}] {2} wird erfasst ...", step, totalSteps, name));
try
{
action();
stopwatch.Stop();
document.SectionStatuses.Add(new SectionStatus
{
Name = name,
Status = "Erfolgreich",
Message = "Abschnitt wurde vollständig ausgeführt.",
DurationMilliseconds = stopwatch.ElapsedMilliseconds
});
logger.Success(string.Format(
"[{0}/{1}] {2} abgeschlossen ({3} ms).",
step,
totalSteps,
name,
stopwatch.ElapsedMilliseconds));
}
catch (Exception exception)
{
stopwatch.Stop();
document.SectionStatuses.Add(new SectionStatus
{
Name = name,
Status = "Teilweise",
Message = exception.Message,
DurationMilliseconds = stopwatch.ElapsedMilliseconds
});
document.Findings.Add(new Finding
{
Severity = "Fehler",
Area = name,
Message = "Der Abschnitt konnte nicht vollständig erfasst werden.",
TechnicalDetail = exception.GetType().Name + ": " + exception.Message,
Recommendation = "Berechtigungen, lokale Datenquelle und Laufprotokoll prüfen; die übrigen Abschnitte sind weiterhin gültig."
});
logger.Error(string.Format("[{0}/{1}] {2} fehlgeschlagen.", step, totalSteps, name), exception);
}
}
}
}
@@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace BizTalkIisEnvironmentInventory.Infrastructure
{
/// <summary>
/// Entfernt Kennwörter, Tokens und verschlüsselte Nutzdaten aus Konfigurationsdarstellungen.
/// </summary>
internal static class SensitiveDataSanitizer
{
private static readonly string[] SensitiveAttributeFragments =
{
"password",
"pwd",
"secret",
"token",
"connectionstring",
"privatekey",
"validationkey",
"decryptionkey"
};
/// <summary>
/// Erstellt eine bereinigte Kopie eines XML-Elements.
/// </summary>
/// <param name="source">Zu bereinigendes Element.</param>
/// <returns>Neue XML-Struktur ohne sensible Attributwerte oder verschlüsselte Blobs.</returns>
internal static XElement SanitizeXml(XElement source)
{
var copy = new XElement(source);
foreach (var element in copy.DescendantsAndSelf())
{
foreach (var attribute in element.Attributes().ToList())
{
if (IsSensitiveName(attribute.Name.LocalName))
{
attribute.Value = "[ENTFERNT]";
}
}
if (IsEncryptedPayloadElement(element.Name.LocalName))
{
element.RemoveNodes();
element.Value = "[VERSCHLUESSELTE NUTZDATEN NICHT DOKUMENTIERT]";
}
}
return copy;
}
/// <summary>
/// Prüft, ob ein Attributname typischerweise ein Secret bezeichnet.
/// </summary>
/// <param name="name">Attributname.</param>
/// <returns><c>true</c>, wenn der Wert entfernt werden muss.</returns>
internal static bool IsSensitiveName(string name)
{
var normalized = (name ?? string.Empty).Replace("-", string.Empty).Replace("_", string.Empty);
return SensitiveAttributeFragments.Any(
item => normalized.IndexOf(item, StringComparison.OrdinalIgnoreCase) >= 0);
}
/// <summary>
/// Erkennt XML-Elemente, deren Inhalt ein geschützter Konfigurationsblob ist.
/// </summary>
/// <param name="name">Lokaler XML-Elementname.</param>
/// <returns><c>true</c> für bekannte verschlüsselte Payload-Elemente.</returns>
private static bool IsEncryptedPayloadElement(string name)
{
return string.Equals(name, "EncryptedData", StringComparison.OrdinalIgnoreCase)
|| string.Equals(name, "CipherData", StringComparison.OrdinalIgnoreCase)
|| string.Equals(name, "CipherValue", StringComparison.OrdinalIgnoreCase);
}
}
}
@@ -0,0 +1,314 @@
using System;
using System.Collections.Generic;
namespace BizTalkIisEnvironmentInventory.Models
{
/// <summary>
/// Vollständiges Ergebnis einer lokalen Bestandsaufnahme.
/// </summary>
internal sealed class InventoryDocument
{
/// <summary>
/// Initialisiert alle Abschnittslisten der Bestandsaufnahme.
/// </summary>
public InventoryDocument()
{
System = new SystemInventory();
Iis = new IisInventory();
Certificates = new List<CertificateRecord>();
Security = new SecurityInventory();
BizTalk = new BizTalkInventory();
Findings = new List<Finding>();
SectionStatuses = new List<SectionStatus>();
}
public string EnvironmentName { get; set; }
public string ComputerName { get; set; }
public DateTime StartedUtc { get; set; }
public DateTime CompletedUtc { get; set; }
public SystemInventory System { get; set; }
public IisInventory Iis { get; set; }
public List<CertificateRecord> Certificates { get; set; }
public SecurityInventory Security { get; set; }
public BizTalkInventory BizTalk { get; set; }
public List<Finding> Findings { get; private set; }
public List<SectionStatus> SectionStatuses { get; private set; }
}
internal sealed class SystemInventory
{
/// <summary>
/// Initialisiert die Listen für Systemeigenschaften und Features.
/// </summary>
public SystemInventory()
{
Properties = new List<NameValueRecord>();
InstalledFeatures = new List<NameValueRecord>();
}
public List<NameValueRecord> Properties { get; private set; }
public List<NameValueRecord> InstalledFeatures { get; private set; }
}
internal sealed class IisInventory
{
/// <summary>
/// Initialisiert alle IIS-Ergebnislisten.
/// </summary>
public IisInventory()
{
ApplicationPools = new List<ApplicationPoolRecord>();
Sites = new List<SiteRecord>();
GlobalSections = new List<ConfigurationSectionRecord>();
EncryptionProviders = new List<EncryptionProviderRecord>();
}
public string ConfigurationPath { get; set; }
public DateTime? ConfigurationLastWriteUtc { get; set; }
public string SanitizedConfigurationSha256 { get; set; }
public List<ApplicationPoolRecord> ApplicationPools { get; private set; }
public List<SiteRecord> Sites { get; private set; }
public List<ConfigurationSectionRecord> GlobalSections { get; private set; }
public List<EncryptionProviderRecord> EncryptionProviders { get; private set; }
}
internal sealed class ApplicationPoolRecord
{
public string Name { get; set; }
public string ManagedRuntimeVersion { get; set; }
public string ManagedPipelineMode { get; set; }
public string AutoStart { get; set; }
public string StartMode { get; set; }
public string IdentityType { get; set; }
public string UserName { get; set; }
public string Enable32BitAppOnWin64 { get; set; }
public List<NameValueRecord> AdditionalSettings { get; set; } = new List<NameValueRecord>();
}
internal sealed class SiteRecord
{
/// <summary>
/// Initialisiert Binding- und Anwendungslisten einer Site.
/// </summary>
public SiteRecord()
{
Bindings = new List<BindingRecord>();
Applications = new List<WebApplicationRecord>();
}
public string Name { get; set; }
public string Id { get; set; }
public string ServerAutoStart { get; set; }
public string LogDirectory { get; set; }
public List<BindingRecord> Bindings { get; private set; }
public List<WebApplicationRecord> Applications { get; private set; }
}
internal sealed class BindingRecord
{
public string SiteName { get; set; }
public string Protocol { get; set; }
public string BindingInformation { get; set; }
public string CertificateHash { get; set; }
public string CertificateStoreName { get; set; }
public string SslFlags { get; set; }
}
internal sealed class WebApplicationRecord
{
/// <summary>
/// Initialisiert Listen für virtuelle Verzeichnisse, Dateien und ACLs.
/// </summary>
public WebApplicationRecord()
{
VirtualDirectories = new List<VirtualDirectoryRecord>();
ContentFiles = new List<ContentFileRecord>();
AccessRules = new List<AccessRuleRecord>();
}
public string SiteName { get; set; }
public string Path { get; set; }
public string ApplicationPool { get; set; }
public string EnabledProtocols { get; set; }
public string PhysicalPath { get; set; }
public bool PhysicalPathExists { get; set; }
public int TotalFiles { get; set; }
public long TotalBytes { get; set; }
public bool ManifestTruncated { get; set; }
public DateTime? LatestWriteUtc { get; set; }
public List<VirtualDirectoryRecord> VirtualDirectories { get; private set; }
public List<ContentFileRecord> ContentFiles { get; private set; }
public List<AccessRuleRecord> AccessRules { get; private set; }
}
internal sealed class VirtualDirectoryRecord
{
public string Path { get; set; }
public string PhysicalPath { get; set; }
public string UserName { get; set; }
}
internal sealed class ContentFileRecord
{
public string RelativePath { get; set; }
public long SizeBytes { get; set; }
public DateTime LastWriteUtc { get; set; }
public string Sha256 { get; set; }
}
internal sealed class AccessRuleRecord
{
public string Target { get; set; }
public string Identity { get; set; }
public string Rights { get; set; }
public string AccessType { get; set; }
public bool IsInherited { get; set; }
public string Inheritance { get; set; }
}
internal sealed class ConfigurationSectionRecord
{
public string Path { get; set; }
public string OverrideModeDefault { get; set; }
public string AllowDefinition { get; set; }
public bool IsEncrypted { get; set; }
public int ElementCount { get; set; }
public string SafeSummary { get; set; }
}
internal sealed class EncryptionProviderRecord
{
public string Name { get; set; }
public string Type { get; set; }
public string KeyContainerName { get; set; }
public string UseMachineContainer { get; set; }
public string Description { get; set; }
}
internal sealed class CertificateRecord
{
/// <summary>
/// Initialisiert Key-Usage- und ACL-Listen eines Zertifikats.
/// </summary>
public CertificateRecord()
{
EnhancedKeyUsages = new List<string>();
KeyAccessRules = new List<AccessRuleRecord>();
}
public string StoreLocation { get; set; }
public string StoreName { get; set; }
public string Subject { get; set; }
public string Issuer { get; set; }
public string Thumbprint { get; set; }
public string SerialNumber { get; set; }
public DateTime NotBefore { get; set; }
public DateTime NotAfter { get; set; }
public string SignatureAlgorithm { get; set; }
public string PublicKeyAlgorithm { get; set; }
public int PublicKeySize { get; set; }
public bool HasPrivateKey { get; set; }
public string PrivateKeyExportable { get; set; }
public string PrivateKeyProvider { get; set; }
public string PrivateKeyContainer { get; set; }
public string PrivateKeyFile { get; set; }
public bool UsedByIisBinding { get; set; }
public List<string> EnhancedKeyUsages { get; private set; }
public List<AccessRuleRecord> KeyAccessRules { get; private set; }
}
internal sealed class SecurityInventory
{
/// <summary>
/// Initialisiert die Listen der lokalen Sicherheitsaufnahme.
/// </summary>
public SecurityInventory()
{
ServiceAccounts = new List<ServiceAccountRecord>();
UserRights = new List<UserRightRecord>();
LocalPolicy = new List<NameValueRecord>();
}
public List<ServiceAccountRecord> ServiceAccounts { get; private set; }
public List<UserRightRecord> UserRights { get; private set; }
public List<NameValueRecord> LocalPolicy { get; private set; }
}
internal sealed class ServiceAccountRecord
{
public string Source { get; set; }
public string Name { get; set; }
public string DisplayName { get; set; }
public string Account { get; set; }
public string State { get; set; }
public string StartMode { get; set; }
public string Path { get; set; }
}
internal sealed class UserRightRecord
{
public string Right { get; set; }
public string Accounts { get; set; }
}
internal sealed class BizTalkInventory
{
/// <summary>
/// Initialisiert alle Listen des BizTalk-Inventars.
/// </summary>
public BizTalkInventory()
{
RegistryValues = new List<NameValueRecord>();
InstalledProducts = new List<NameValueRecord>();
Components = new List<NameValueRecord>();
WmiClasses = new List<NameValueRecord>();
}
public bool WmiNamespaceAvailable { get; set; }
public List<NameValueRecord> RegistryValues { get; private set; }
public List<NameValueRecord> InstalledProducts { get; private set; }
public List<NameValueRecord> Components { get; private set; }
public List<NameValueRecord> WmiClasses { get; private set; }
}
internal sealed class NameValueRecord
{
/// <summary>
/// Initialisiert einen leeren Name/Wert-Datensatz.
/// </summary>
public NameValueRecord()
{
}
/// <summary>
/// Initialisiert einen Name/Wert-Datensatz.
/// </summary>
/// <param name="name">Eigenschaftsname.</param>
/// <param name="value">Eigenschaftswert.</param>
public NameValueRecord(string name, string value)
{
Name = name;
Value = value;
}
public string Name { get; set; }
public string Value { get; set; }
}
internal sealed class Finding
{
public string Severity { get; set; }
public string Area { get; set; }
public string Message { get; set; }
public string TechnicalDetail { get; set; }
public string Recommendation { get; set; }
}
internal sealed class SectionStatus
{
public string Name { get; set; }
public string Status { get; set; }
public string Message { get; set; }
public long DurationMilliseconds { get; set; }
}
}
@@ -0,0 +1,216 @@
using System;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Xml.Linq;
using BizTalkIisEnvironmentInventory.Collectors;
using BizTalkIisEnvironmentInventory.Configuration;
using BizTalkIisEnvironmentInventory.Infrastructure;
using BizTalkIisEnvironmentInventory.Models;
using BizTalkIisEnvironmentInventory.Reporting;
namespace BizTalkIisEnvironmentInventory
{
/// <summary>
/// Einstiegspunkt und Ablaufsteuerung der Bestandsaufnahme.
/// </summary>
internal static class Program
{
/// <summary>
/// Analysiert Optionen, führt alle read-only Collectoren aus und erzeugt das Word-Dokument.
/// </summary>
/// <param name="args">Kommandozeilenargumente.</param>
/// <returns>0 bei vollständigem Erfolg, 1 bei Teilfehler mit Report, 2 bei fatalem Fehler.</returns>
private static int Main(string[] args)
{
CommandLineOptions commandLine;
try
{
commandLine = CommandLineOptions.Parse(args);
}
catch (ArgumentException exception)
{
Console.Error.WriteLine("Fehler: " + exception.Message);
PrintHelp();
return 2;
}
if (commandLine.ShowHelp)
{
PrintHelp();
return 0;
}
if (commandLine.SelfTest)
{
return RunSelfTest();
}
try
{
Directory.CreateDirectory(commandLine.OutputDirectory);
var stamp = DateTime.Now.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture);
var baseName = "IIS-Dokumentation-" + commandLine.EnvironmentName + "-"
+ CommandLineOptions.SanitizeEnvironment(Environment.MachineName) + "-" + stamp;
var logPath = Path.Combine(commandLine.OutputDirectory, baseName + ".log");
using (var logger = new FileLogger(logPath))
{
return Run(commandLine, logger, baseName);
}
}
catch (Exception exception)
{
Console.Error.WriteLine("Fataler Fehler: " + exception);
return 2;
}
}
/// <summary>
/// Führt die Collector-Pipeline aus und schreibt den Bericht.
/// </summary>
/// <param name="commandLine">Validierte Laufoptionen.</param>
/// <param name="logger">Gemeinsames Konsolen-/Dateiprotokoll.</param>
/// <param name="baseName">Eindeutiger Basisdateiname.</param>
/// <returns>0 bei vollständigem Erfolg, sonst 1 bei verwertbarem Teilreport.</returns>
private static int Run(CommandLineOptions commandLine, FileLogger logger, string baseName)
{
const int totalSteps = 6;
var options = CollectorOptions.Load(commandLine);
var document = new InventoryDocument
{
EnvironmentName = commandLine.EnvironmentName,
ComputerName = Environment.MachineName,
StartedUtc = DateTime.UtcNow
};
logger.Info("BizTalk IIS Environment Inventory 1.0 startet.");
logger.Info("Modus: read-only; keine Kennwörter, verschlüsselten Payloads oder privaten Schlüssel im Report.");
logger.Info("Umgebung: " + commandLine.EnvironmentName);
logger.Info("Ausgabe: " + commandLine.OutputDirectory);
logger.Info("Dateimanifest: " + (options.MaxFilesPerApplication > 0
? "max. " + options.MaxFilesPerApplication + " Einträge je Anwendung"
: "deaktiviert"));
logger.Info("Datei-Hashes: " + (options.IncludeFileHashes ? "aktiv" : "deaktiviert"));
SafeCollector.Run(document, logger, 1, totalSteps, "Windows und Rollen",
() => new SystemCollector(options).Collect(document.System));
SafeCollector.Run(document, logger, 2, totalSteps, "IIS und Webinhalte",
() => new IisCollector(options).Collect(document.Iis, document.Findings, commandLine.IisConfigPath));
SafeCollector.Run(document, logger, 3, totalSteps, "Zertifikate und Schlüsselmetadaten",
() => new CertificateCollector(options).Collect(document.Certificates, document.Iis, document.Findings));
SafeCollector.Run(document, logger, 4, totalSteps, "Dienstkonten und lokale Sicherheit",
() => new SecurityCollector(options).Collect(document.Security, document.Iis, document.Findings));
SafeCollector.Run(document, logger, 5, totalSteps, "BizTalk-Komponenten",
() => new BizTalkCollector(options).Collect(document.BizTalk, document.Findings));
document.CompletedUtc = DateTime.UtcNow;
var reportPath = Path.Combine(commandLine.OutputDirectory, baseName + ".docx");
SafeCollector.Run(document, logger, 6, totalSteps, "Word-Dokument",
() => new DocxReportWriter().Write(document, reportPath));
var partial = document.SectionStatuses.Any(
item => !string.Equals(item.Status, "Erfolgreich", StringComparison.OrdinalIgnoreCase));
logger.Info("Auffälligkeiten: " + document.Findings.Count.ToString(CultureInfo.InvariantCulture));
logger.Success("Word-Dokument: " + reportPath);
logger.Info("Laufprotokoll: " + logger.LogPath);
logger.Info(partial
? "Erfassung mit Teilfehlern abgeschlossen (ExitCode 1)."
: "Erfassung vollständig abgeschlossen (ExitCode 0).");
return partial ? 1 : 0;
}
/// <summary>
/// Führt plattformneutrale interne Prüfungen ohne Zugriff auf IIS oder BizTalk aus.
/// </summary>
/// <returns>0 bei Erfolg, sonst 2.</returns>
private static int RunSelfTest()
{
string temporaryDocument = null;
try
{
var environment = CommandLineOptions.SanitizeEnvironment(" acc / produkt ");
if (!string.Equals(environment, "ACC-PRODUKT", StringComparison.Ordinal))
{
throw new InvalidOperationException("Umgebungsnormalisierung fehlgeschlagen.");
}
var xml = System.Xml.Linq.XElement.Parse(
"<root password=\"secret\"><child token=\"abc\"/><EncryptedData>payload</EncryptedData></root>");
var sanitized = SensitiveDataSanitizer.SanitizeXml(xml).ToString();
if (sanitized.Contains("secret") || sanitized.Contains("payload") || sanitized.Contains("abc"))
{
throw new InvalidOperationException("Secret-Bereinigung fehlgeschlagen.");
}
var document = new InventoryDocument
{
EnvironmentName = "<ACC>",
ComputerName = "TEST",
StartedUtc = DateTime.UtcNow,
CompletedUtc = DateTime.UtcNow
};
temporaryDocument = Path.Combine(
Path.GetTempPath(),
"BizTalkIisInventory-SelfTest-" + Guid.NewGuid().ToString("N") + ".docx");
new DocxReportWriter().Write(document, temporaryDocument);
using (var archive = ZipFile.OpenRead(temporaryDocument))
{
var documentEntry = archive.GetEntry("word/document.xml");
var stylesEntry = archive.GetEntry("word/styles.xml");
if (documentEntry == null || stylesEntry == null)
{
throw new InvalidOperationException("Erforderliche DOCX-Parts fehlen.");
}
using (var stream = documentEntry.Open())
{
XDocument.Load(stream);
}
}
Console.WriteLine("Self-Test erfolgreich.");
return 0;
}
catch (Exception exception)
{
Console.Error.WriteLine("Self-Test fehlgeschlagen: " + exception);
return 2;
}
finally
{
if (!string.IsNullOrWhiteSpace(temporaryDocument) && File.Exists(temporaryDocument))
{
try
{
File.Delete(temporaryDocument);
}
catch
{
// Eine fehlgeschlagene Temp-Bereinigung ändert das Self-Test-Ergebnis nicht.
}
}
}
}
/// <summary>
/// Gibt die deutsche Kommandozeilenhilfe aus.
/// </summary>
private static void PrintHelp()
{
Console.WriteLine("BizTalk IIS Environment Inventory");
Console.WriteLine();
Console.WriteLine("Aufruf:");
Console.WriteLine(" BizTalkIisEnvironmentInventory.exe --environment ACC --output C:\\IIS-Doku\\ACC");
Console.WriteLine();
Console.WriteLine("Optionen:");
Console.WriteLine(" -e, --environment NAME Umgebungsname, z. B. ACC oder PROD");
Console.WriteLine(" -o, --output PFAD Ausgabeordner für DOCX und Log");
Console.WriteLine(" --iis-config DATEI Alternative applicationHost.config (Offline-Test)");
Console.WriteLine(" --include-file-hashes SHA-256 für jede manifestierte Webdatei");
Console.WriteLine(" --skip-content-manifest Nur Pfade/ACLs, keine Dateiliste");
Console.WriteLine(" --self-test Prüft Parser und DOCX-Paketstruktur");
Console.WriteLine(" -h, --help Diese Hilfe");
}
}
}
@@ -0,0 +1,14 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("BizTalk IIS Environment Inventory")]
[assembly: AssemblyDescription("Read-only IIS and BizTalk environment documentation collector")]
[assembly: AssemblyCompany("JR IT Services")]
[assembly: AssemblyProduct("BizTalk IIS Environment Inventory")]
[assembly: ComVisible(false)]
[assembly: Guid("66e7a524-f96b-46b9-a8b1-8f967c9fb77c")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: InternalsVisibleTo("BizTalkIisEnvironmentInventory.Tests")]
@@ -0,0 +1,977 @@
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 BizTalkIisEnvironmentInventory.Models;
namespace BizTalkIisEnvironmentInventory.Reporting
{
/// <summary>
/// Erzeugt ein standardkonformes Microsoft-Word-Dokument im Office-Open-XML-Format ohne Office-Installation.
/// </summary>
internal sealed class DocxReportWriter
{
private const string WordNamespace = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
private const string RelationshipNamespace = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
private const string PackageRelationshipNamespace = "http://schemas.openxmlformats.org/package/2006/relationships";
private const string ContentTypeNamespace = "http://schemas.openxmlformats.org/package/2006/content-types";
/// <summary>
/// Schreibt den vollständigen DOCX-Bericht atomar in die Zieldatei.
/// </summary>
/// <param name="document">Abgeschlossene Bestandsaufnahme.</param>
/// <param name="path">Vollständiger Zielpfad mit Erweiterung <c>.docx</c>.</param>
public void Write(InventoryDocument document, string path)
{
if (document == null)
{
throw new ArgumentNullException("document");
}
Directory.CreateDirectory(Path.GetDirectoryName(path));
var temporaryPath = path + "." + Guid.NewGuid().ToString("N") + ".tmp";
try
{
using (var stream = new FileStream(temporaryPath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None))
using (var archive = new ZipArchive(stream, ZipArchiveMode.Create, false, Encoding.UTF8))
{
WriteContentTypes(archive);
WritePackageRelationships(archive);
WriteDocumentRelationships(archive);
WriteCoreProperties(archive, document);
WriteApplicationProperties(archive);
WriteStyles(archive);
WriteMainDocument(archive, document);
}
if (File.Exists(path))
{
File.Replace(temporaryPath, path, null);
}
else
{
File.Move(temporaryPath, path);
}
}
finally
{
if (File.Exists(temporaryPath))
{
File.Delete(temporaryPath);
}
}
}
/// <summary>
/// Schreibt die MIME-Zuordnungen des Office-Open-XML-Pakets.
/// </summary>
/// <param name="archive">Geöffnetes DOCX-ZIP-Archiv.</param>
private static void WriteContentTypes(ZipArchive archive)
{
WriteXmlEntry(archive, "[Content_Types].xml", writer =>
{
writer.WriteStartElement("Types", ContentTypeNamespace);
WriteContentTypeDefault(writer, "rels", "application/vnd.openxmlformats-package.relationships+xml");
WriteContentTypeDefault(writer, "xml", "application/xml");
WriteContentTypeOverride(writer, "/word/document.xml",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml");
WriteContentTypeOverride(writer, "/word/styles.xml",
"application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml");
WriteContentTypeOverride(writer, "/docProps/core.xml",
"application/vnd.openxmlformats-package.core-properties+xml");
WriteContentTypeOverride(writer, "/docProps/app.xml",
"application/vnd.openxmlformats-officedocument.extended-properties+xml");
writer.WriteEndElement();
});
}
/// <summary>
/// Schreibt eine Default-MIME-Zuordnung.
/// </summary>
/// <param name="writer">XML-Writer.</param>
/// <param name="extension">Dateierweiterung.</param>
/// <param name="contentType">MIME-Typ.</param>
private static void WriteContentTypeDefault(XmlWriter writer, string extension, string contentType)
{
writer.WriteStartElement("Default", ContentTypeNamespace);
writer.WriteAttributeString("Extension", extension);
writer.WriteAttributeString("ContentType", contentType);
writer.WriteEndElement();
}
/// <summary>
/// Schreibt eine part-spezifische MIME-Zuordnung.
/// </summary>
/// <param name="writer">XML-Writer.</param>
/// <param name="partName">Absoluter Paketpart.</param>
/// <param name="contentType">MIME-Typ.</param>
private static void WriteContentTypeOverride(XmlWriter writer, string partName, string contentType)
{
writer.WriteStartElement("Override", ContentTypeNamespace);
writer.WriteAttributeString("PartName", partName);
writer.WriteAttributeString("ContentType", contentType);
writer.WriteEndElement();
}
/// <summary>
/// Schreibt die Beziehungen von der Paketwurzel zu Dokument und Eigenschaften.
/// </summary>
/// <param name="archive">Geöffnetes DOCX-ZIP-Archiv.</param>
private static void WritePackageRelationships(ZipArchive archive)
{
WriteXmlEntry(archive, "_rels/.rels", writer =>
{
writer.WriteStartElement("Relationships", PackageRelationshipNamespace);
WriteRelationship(writer, "rId1",
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",
"word/document.xml");
WriteRelationship(writer, "rId2",
"http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties",
"docProps/core.xml");
WriteRelationship(writer, "rId3",
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties",
"docProps/app.xml");
writer.WriteEndElement();
});
}
/// <summary>
/// Schreibt die Beziehungen des Hauptdokuments.
/// </summary>
/// <param name="archive">Geöffnetes DOCX-ZIP-Archiv.</param>
private static void WriteDocumentRelationships(ZipArchive archive)
{
WriteXmlEntry(archive, "word/_rels/document.xml.rels", writer =>
{
writer.WriteStartElement("Relationships", PackageRelationshipNamespace);
WriteRelationship(writer, "rIdStyles",
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles",
"styles.xml");
writer.WriteEndElement();
});
}
/// <summary>
/// Schreibt eine OOXML-Beziehung.
/// </summary>
/// <param name="writer">XML-Writer.</param>
/// <param name="id">Beziehungs-ID.</param>
/// <param name="type">Beziehungstyp.</param>
/// <param name="target">Relatives Ziel.</param>
private static void WriteRelationship(XmlWriter writer, string id, string type, string target)
{
writer.WriteStartElement("Relationship", PackageRelationshipNamespace);
writer.WriteAttributeString("Id", id);
writer.WriteAttributeString("Type", type);
writer.WriteAttributeString("Target", target);
writer.WriteEndElement();
}
/// <summary>
/// Schreibt Titel, Ersteller und Zeitstempel des Dokuments.
/// </summary>
/// <param name="archive">Geöffnetes DOCX-ZIP-Archiv.</param>
/// <param name="document">Bestandsaufnahme.</param>
private static void WriteCoreProperties(ZipArchive archive, InventoryDocument document)
{
WriteXmlEntry(archive, "docProps/core.xml", writer =>
{
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", "dcmitype", null, "http://purl.org/dc/dcmitype/");
writer.WriteAttributeString("xmlns", "xsi", null, "http://www.w3.org/2001/XMLSchema-instance");
writer.WriteElementString("dc", "title", "http://purl.org/dc/elements/1.1/",
"IIS- und BizTalk-Dokumentation " + document.EnvironmentName);
writer.WriteElementString("dc", "creator", "http://purl.org/dc/elements/1.1/",
"BizTalk IIS Environment Inventory");
writer.WriteElementString("cp", "lastModifiedBy",
"http://schemas.openxmlformats.org/package/2006/metadata/core-properties",
"BizTalk IIS Environment Inventory");
WriteDublinCoreDate(writer, "created", document.StartedUtc);
WriteDublinCoreDate(writer, "modified", document.CompletedUtc);
writer.WriteEndElement();
});
}
/// <summary>
/// Schreibt einen typisierten Dublin-Core-Zeitwert.
/// </summary>
/// <param name="writer">XML-Writer.</param>
/// <param name="name">Elementname.</param>
/// <param name="value">UTC-Zeitwert.</param>
private static void WriteDublinCoreDate(XmlWriter writer, string name, DateTime value)
{
writer.WriteStartElement("dcterms", name, "http://purl.org/dc/terms/");
writer.WriteAttributeString("xsi", "type", "http://www.w3.org/2001/XMLSchema-instance", "dcterms:W3CDTF");
writer.WriteString(value.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture));
writer.WriteEndElement();
}
/// <summary>
/// Schreibt die Office-Anwendungseigenschaften.
/// </summary>
/// <param name="archive">Geöffnetes DOCX-ZIP-Archiv.</param>
private static void WriteApplicationProperties(ZipArchive archive)
{
WriteXmlEntry(archive, "docProps/app.xml", writer =>
{
const string ns = "http://schemas.openxmlformats.org/officeDocument/2006/extended-properties";
writer.WriteStartElement("Properties", ns);
writer.WriteAttributeString("xmlns", "vt", null,
"http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes");
writer.WriteElementString("Application", ns, "BizTalk IIS Environment Inventory");
writer.WriteElementString("AppVersion", ns, "1.0");
writer.WriteElementString("Company", ns, "JR IT Services");
writer.WriteEndElement();
});
}
/// <summary>
/// Schreibt die im Bericht verwendeten Word-Formatvorlagen.
/// </summary>
/// <param name="archive">Geöffnetes DOCX-ZIP-Archiv.</param>
private static void WriteStyles(ZipArchive archive)
{
WriteXmlEntry(archive, "word/styles.xml", writer =>
{
writer.WriteStartElement("w", "styles", WordNamespace);
WriteParagraphStyle(writer, "Normal", "Standard", 20, "1F2937", false, 0, 0);
WriteParagraphStyle(writer, "Title", "Titel", 42, "0B4F78", true, 220, 160);
WriteParagraphStyle(writer, "Subtitle", "Untertitel", 22, "526575", false, 0, 160);
WriteParagraphStyle(writer, "Heading1", "Überschrift 1", 32, "0B4F78", true, 360, 160);
WriteParagraphStyle(writer, "Heading2", "Überschrift 2", 26, "176B96", true, 280, 120);
WriteParagraphStyle(writer, "Heading3", "Überschrift 3", 22, "1F2937", true, 220, 80);
WriteTableStyle(writer);
writer.WriteEndElement();
});
}
/// <summary>
/// Schreibt eine Absatzformatvorlage.
/// </summary>
/// <param name="writer">XML-Writer.</param>
/// <param name="styleId">Interne Style-ID.</param>
/// <param name="name">Anzeigename.</param>
/// <param name="fontSizeHalfPoints">Schriftgröße in halben Punkten.</param>
/// <param name="color">RGB-Farbe.</param>
/// <param name="bold">Fettdruck.</param>
/// <param name="spaceBefore">Abstand davor in Twips.</param>
/// <param name="spaceAfter">Abstand danach in Twips.</param>
private static void WriteParagraphStyle(
XmlWriter writer,
string styleId,
string name,
int fontSizeHalfPoints,
string color,
bool bold,
int spaceBefore,
int spaceAfter)
{
writer.WriteStartElement("w", "style", WordNamespace);
writer.WriteAttributeString("w", "type", WordNamespace, "paragraph");
writer.WriteAttributeString("w", "styleId", WordNamespace, styleId);
writer.WriteStartElement("w", "name", WordNamespace);
writer.WriteAttributeString("w", "val", WordNamespace, name);
writer.WriteEndElement();
writer.WriteStartElement("w", "pPr", WordNamespace);
writer.WriteStartElement("w", "spacing", WordNamespace);
writer.WriteAttributeString("w", "before", WordNamespace, spaceBefore.ToString(CultureInfo.InvariantCulture));
writer.WriteAttributeString("w", "after", WordNamespace, spaceAfter.ToString(CultureInfo.InvariantCulture));
writer.WriteEndElement();
writer.WriteEndElement();
writer.WriteStartElement("w", "rPr", WordNamespace);
if (bold)
{
writer.WriteElementString("w", "b", WordNamespace, string.Empty);
}
writer.WriteStartElement("w", "color", WordNamespace);
writer.WriteAttributeString("w", "val", WordNamespace, color);
writer.WriteEndElement();
writer.WriteStartElement("w", "sz", WordNamespace);
writer.WriteAttributeString("w", "val", WordNamespace,
fontSizeHalfPoints.ToString(CultureInfo.InvariantCulture));
writer.WriteEndElement();
writer.WriteEndElement();
writer.WriteEndElement();
}
/// <summary>
/// Definiert den Tabellenstil mit sichtbaren Gitternetzlinien.
/// </summary>
/// <param name="writer">XML-Writer.</param>
private static void WriteTableStyle(XmlWriter writer)
{
writer.WriteStartElement("w", "style", WordNamespace);
writer.WriteAttributeString("w", "type", WordNamespace, "table");
writer.WriteAttributeString("w", "styleId", WordNamespace, "InventoryTable");
writer.WriteStartElement("w", "name", WordNamespace);
writer.WriteAttributeString("w", "val", WordNamespace, "Inventartabelle");
writer.WriteEndElement();
writer.WriteStartElement("w", "tblPr", WordNamespace);
writer.WriteStartElement("w", "tblBorders", WordNamespace);
foreach (var edge in new[] { "top", "left", "bottom", "right", "insideH", "insideV" })
{
writer.WriteStartElement("w", edge, WordNamespace);
writer.WriteAttributeString("w", "val", WordNamespace, "single");
writer.WriteAttributeString("w", "sz", WordNamespace, "4");
writer.WriteAttributeString("w", "color", WordNamespace, "CBD5E1");
writer.WriteEndElement();
}
writer.WriteEndElement();
writer.WriteEndElement();
writer.WriteEndElement();
}
/// <summary>
/// Schreibt den fachlichen Inhalt des Word-Dokuments.
/// </summary>
/// <param name="archive">Geöffnetes DOCX-ZIP-Archiv.</param>
/// <param name="document">Bestandsaufnahme.</param>
private static void WriteMainDocument(ZipArchive archive, InventoryDocument document)
{
WriteXmlEntry(archive, "word/document.xml", writer =>
{
writer.WriteStartElement("w", "document", WordNamespace);
writer.WriteAttributeString("xmlns", "r", null, RelationshipNamespace);
writer.WriteStartElement("w", "body", WordNamespace);
WriteParagraph(writer, "IIS- und BizTalk-Dokumentation", "Title");
WriteParagraph(writer, document.EnvironmentName + " · " + document.ComputerName, "Subtitle");
WriteParagraph(writer,
"Erstellt am " + document.CompletedUtc.ToLocalTime()
.ToString("yyyy-MM-dd HH:mm:ss zzz", CultureInfo.InvariantCulture),
"Subtitle");
WriteNotice(writer,
"Schutz sensibler Daten: Kennwörter, Tokens, Connection Strings, verschlüsselte Payloads "
+ "und private Schlüssel werden nicht ausgegeben. Dokumentiert werden ausschließlich "
+ "migrationsrelevante Metadaten, Fingerprints, Container und Berechtigungen.");
WriteOverview(writer, document);
WriteFindings(writer, document.Findings);
WriteSystem(writer, document.System);
WriteIis(writer, document.Iis);
WriteCertificates(writer, document.Certificates);
WriteSecurity(writer, document.Security);
WriteBizTalk(writer, document.BizTalk);
WriteParagraph(writer,
"Erzeugt durch BizTalk IIS Environment Inventory · read-only · keine Office-Installation erforderlich",
"Subtitle");
WriteSectionProperties(writer);
writer.WriteEndElement();
writer.WriteEndElement();
});
}
/// <summary>
/// Schreibt Zusammenfassung und Collector-Status.
/// </summary>
/// <param name="writer">XML-Writer.</param>
/// <param name="document">Bestandsaufnahme.</param>
private static void WriteOverview(XmlWriter writer, InventoryDocument document)
{
WriteHeading(writer, 1, "1. Übersicht");
WriteNameValueTable(writer, new[]
{
new NameValueRecord("Umgebung", document.EnvironmentName),
new NameValueRecord("Computer", document.ComputerName),
new NameValueRecord("IIS-Sites", document.Iis.Sites.Count.ToString(CultureInfo.InvariantCulture)),
new NameValueRecord("IIS-Anwendungen",
document.Iis.Sites.Sum(site => site.Applications.Count).ToString(CultureInfo.InvariantCulture)),
new NameValueRecord("Application Pools",
document.Iis.ApplicationPools.Count.ToString(CultureInfo.InvariantCulture)),
new NameValueRecord("Zertifikate", document.Certificates.Count.ToString(CultureInfo.InvariantCulture)),
new NameValueRecord("Auffälligkeiten", document.Findings.Count.ToString(CultureInfo.InvariantCulture)),
new NameValueRecord("Start UTC", FormatUtc(document.StartedUtc)),
new NameValueRecord("Ende UTC", FormatUtc(document.CompletedUtc)),
new NameValueRecord("Dauer",
(document.CompletedUtc - document.StartedUtc).ToString(@"hh\:mm\:ss", CultureInfo.InvariantCulture))
});
WriteHeading(writer, 2, "Collector-Status");
WriteTable(writer,
new[] { "Abschnitt", "Status", "Dauer", "Meldung" },
document.SectionStatuses.Select(status => new[]
{
status.Name,
status.Status,
status.DurationMilliseconds.ToString(CultureInfo.InvariantCulture) + " ms",
status.Message
}));
}
/// <summary>
/// Schreibt Auffälligkeiten und Maßnahmen.
/// </summary>
/// <param name="writer">XML-Writer.</param>
/// <param name="findings">Auffälligkeiten.</param>
private static void WriteFindings(XmlWriter writer, IList<Finding> findings)
{
WriteHeading(writer, 1, "2. Auffälligkeiten und Hinweise");
if (findings.Count == 0)
{
WriteParagraph(writer, "Keine technischen Auffälligkeiten während der Erfassung.", "Normal");
return;
}
WriteTable(writer,
new[] { "Stufe", "Bereich", "Aussage", "Technik", "Empfehlung" },
findings.OrderBy(item => SeverityOrder(item.Severity)).ThenBy(item => item.Area).Select(item => new[]
{
item.Severity, item.Area, item.Message, item.TechnicalDetail, item.Recommendation
}));
}
/// <summary>
/// Schreibt Windows- und Rolleninformationen.
/// </summary>
/// <param name="writer">XML-Writer.</param>
/// <param name="system">Systeminventar.</param>
private static void WriteSystem(XmlWriter writer, SystemInventory system)
{
WriteHeading(writer, 1, "3. Windows und installierte Rollen");
WriteHeading(writer, 2, "System");
WriteNameValueTable(writer, system.Properties);
WriteHeading(writer, 2, "IIS-/BizTalk-relevante Windows Server Features");
WriteNameValueTable(writer, system.InstalledFeatures);
}
/// <summary>
/// Schreibt IIS-Konfiguration, Topologie, Webinhalte und ACLs.
/// </summary>
/// <param name="writer">XML-Writer.</param>
/// <param name="iis">IIS-Inventar.</param>
private static void WriteIis(XmlWriter writer, IisInventory iis)
{
WriteHeading(writer, 1, "4. IIS-Konfiguration");
WriteNameValueTable(writer, new[]
{
new NameValueRecord("applicationHost.config", iis.ConfigurationPath),
new NameValueRecord("Letzte Änderung UTC",
iis.ConfigurationLastWriteUtc.HasValue ? FormatUtc(iis.ConfigurationLastWriteUtc.Value) : string.Empty),
new NameValueRecord("SHA-256 der bereinigten Konfiguration", iis.SanitizedConfigurationSha256)
});
WriteHeading(writer, 2, "Application Pools");
WriteTable(writer,
new[] { "Name", "Identitätstyp", "Effektives Konto", ".NET", "Pipeline", "StartMode", "32 Bit" },
iis.ApplicationPools.Select(pool => new[]
{
pool.Name, pool.IdentityType, pool.UserName, pool.ManagedRuntimeVersion,
pool.ManagedPipelineMode, pool.StartMode, pool.Enable32BitAppOnWin64
}));
WriteHeading(writer, 2, "Sites, Anwendungen und Webinhalte");
foreach (var site in iis.Sites)
{
WriteHeading(writer, 3, "Site: " + site.Name + " (ID " + site.Id + ")");
WriteNameValueTable(writer, new[]
{
new NameValueRecord("Automatischer Start", site.ServerAutoStart),
new NameValueRecord("Logverzeichnis", site.LogDirectory)
});
WriteParagraph(writer, "Bindings", "Heading3");
WriteTable(writer,
new[] { "Protokoll", "Binding", "Zertifikat", "Store", "SSL Flags" },
site.Bindings.Select(binding => new[]
{
binding.Protocol, binding.BindingInformation, binding.CertificateHash,
binding.CertificateStoreName, binding.SslFlags
}));
foreach (var application in site.Applications)
{
WriteApplication(writer, application);
}
}
WriteHeading(writer, 2, "Globale Konfigurationsabschnitte");
WriteTable(writer,
new[] { "Section", "Override", "Allow Definition", "Verschlüsselt", "Elemente", "Sichere Übersicht" },
iis.GlobalSections.Select(section => new[]
{
section.Path, section.OverrideModeDefault, section.AllowDefinition,
section.IsEncrypted ? "Ja" : "Nein",
section.ElementCount.ToString(CultureInfo.InvariantCulture), section.SafeSummary
}));
WriteHeading(writer, 2, "Konfigurationsverschlüsselung");
WriteTable(writer,
new[] { "Provider", "Typ", "Key Container", "Machine Container", "Hinweis" },
iis.EncryptionProviders.Select(provider => new[]
{
provider.Name, provider.Type, provider.KeyContainerName,
provider.UseMachineContainer, provider.Description
}));
}
/// <summary>
/// Schreibt eine IIS-Anwendung einschließlich Dateimanifest.
/// </summary>
/// <param name="writer">XML-Writer.</param>
/// <param name="application">IIS-Anwendung.</param>
private static void WriteApplication(XmlWriter writer, WebApplicationRecord application)
{
WriteParagraph(writer, "Anwendung: " + application.SiteName + application.Path, "Heading3");
WriteNameValueTable(writer, new[]
{
new NameValueRecord("Application Pool", application.ApplicationPool),
new NameValueRecord("Protokolle", application.EnabledProtocols),
new NameValueRecord("Physischer Pfad", application.PhysicalPath),
new NameValueRecord("Pfad vorhanden", application.PhysicalPathExists ? "Ja" : "Nein"),
new NameValueRecord("Dateien gesamt", application.TotalFiles.ToString("N0",
CultureInfo.GetCultureInfo("de-DE"))),
new NameValueRecord("Größe gesamt", FormatBytes(application.TotalBytes)),
new NameValueRecord("Letzte Dateiänderung UTC",
application.LatestWriteUtc.HasValue ? FormatUtc(application.LatestWriteUtc.Value) : string.Empty),
new NameValueRecord("Manifest begrenzt/unvollständig", application.ManifestTruncated ? "Ja" : "Nein")
});
WriteParagraph(writer, "Virtuelle Verzeichnisse", "Heading3");
WriteTable(writer,
new[] { "Pfad", "Physical Path", "Zugriffsidentität" },
application.VirtualDirectories.Select(item => new[] { item.Path, item.PhysicalPath, item.UserName }));
WriteParagraph(writer, "NTFS-Berechtigungen des Web-Stamms", "Heading3");
WriteAclTable(writer, application.AccessRules);
WriteParagraph(writer,
"Dateimanifest (" + application.ContentFiles.Count.ToString("N0",
CultureInfo.GetCultureInfo("de-DE")) + " Einträge)",
"Heading3");
WriteTable(writer,
new[] { "Relativer Pfad", "Größe", "Letzte Änderung UTC", "SHA-256" },
application.ContentFiles.Select(file => new[]
{
file.RelativePath, FormatBytes(file.SizeBytes), FormatUtc(file.LastWriteUtc), file.Sha256
}));
}
/// <summary>
/// Schreibt Zertifikate und private Key-ACLs.
/// </summary>
/// <param name="writer">XML-Writer.</param>
/// <param name="certificates">Zertifikatsinventar.</param>
private static void WriteCertificates(XmlWriter writer, IList<CertificateRecord> certificates)
{
WriteHeading(writer, 1, "5. Zertifikate und private Schlüssel");
WriteNotice(writer,
"Es werden keine Schlüssel exportiert. „Exportierbar“ wird ausschließlich aus der Provider-Policy gelesen.");
if (certificates.Count == 0)
{
WriteParagraph(writer, "Keine Zertifikate erfasst.", "Normal");
return;
}
foreach (var certificate in certificates)
{
WriteHeading(writer, 3,
(certificate.UsedByIisBinding ? "[IIS] " : string.Empty) + certificate.Subject);
WriteNameValueTable(writer, new[]
{
new NameValueRecord("IIS-Binding", certificate.UsedByIisBinding ? "Ja" : "Nein"),
new NameValueRecord("Store", certificate.StoreLocation + "\\" + certificate.StoreName),
new NameValueRecord("Subject", certificate.Subject),
new NameValueRecord("Issuer", certificate.Issuer),
new NameValueRecord("Thumbprint", certificate.Thumbprint),
new NameValueRecord("Serial", certificate.SerialNumber),
new NameValueRecord("Gültig von",
certificate.NotBefore.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)),
new NameValueRecord("Gültig bis",
certificate.NotAfter.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)),
new NameValueRecord("Public Key",
certificate.PublicKeyAlgorithm + " / " + certificate.PublicKeySize + " Bit"),
new NameValueRecord("Signature", certificate.SignatureAlgorithm),
new NameValueRecord("Enhanced Key Usage", string.Join("; ", certificate.EnhancedKeyUsages)),
new NameValueRecord("Privater Schlüssel vorhanden", certificate.HasPrivateKey ? "Ja" : "Nein"),
new NameValueRecord("Exportierbar", certificate.PrivateKeyExportable),
new NameValueRecord("Key Provider", certificate.PrivateKeyProvider),
new NameValueRecord("Key Container", certificate.PrivateKeyContainer),
new NameValueRecord("Key-Datei", certificate.PrivateKeyFile)
});
WriteParagraph(writer, "ACL des privaten Schlüssels", "Heading3");
WriteAclTable(writer, certificate.KeyAccessRules);
}
}
/// <summary>
/// Schreibt Dienstidentitäten und lokale Sicherheitsrichtlinien.
/// </summary>
/// <param name="writer">XML-Writer.</param>
/// <param name="security">Security-Inventar.</param>
private static void WriteSecurity(XmlWriter writer, SecurityInventory security)
{
WriteHeading(writer, 1, "6. NTFS, Dienstkonten und lokale Sicherheit");
WriteHeading(writer, 2, "Dienstidentitäten");
WriteTable(writer,
new[] { "Quelle", "Name", "Anzeigename", "Konto", "Status", "Startmodus", "Pfad" },
security.ServiceAccounts.Select(item => new[]
{
item.Source, item.Name, item.DisplayName, item.Account,
item.State, item.StartMode, item.Path
}));
WriteHeading(writer, 2, "User Rights Assignment");
WriteTable(writer,
new[] { "Benutzerrecht", "Konten/SIDs" },
security.UserRights.Select(item => new[] { item.Right, item.Accounts }));
WriteHeading(writer, 2, "Lokale Security-/Audit-Policy");
WriteNameValueTable(writer, security.LocalPolicy);
}
/// <summary>
/// Schreibt BizTalk-Installations- und WMI-Daten.
/// </summary>
/// <param name="writer">XML-Writer.</param>
/// <param name="bizTalk">BizTalk-Inventar.</param>
private static void WriteBizTalk(XmlWriter writer, BizTalkInventory bizTalk)
{
WriteHeading(writer, 1, "7. BizTalk-spezifische Komponenten");
WriteNameValueTable(writer, new[]
{
new NameValueRecord("WMI root\\MicrosoftBizTalkServer",
bizTalk.WmiNamespaceAvailable ? "Erreichbar" : "Nicht erreichbar")
});
WriteHeading(writer, 2, "Installierte Produkte");
WriteNameValueTable(writer, bizTalk.InstalledProducts);
WriteHeading(writer, 2, "Registry-Metadaten");
WriteNameValueTable(writer, bizTalk.RegistryValues);
WriteHeading(writer, 2, "Zentrale Binärversionen");
WriteNameValueTable(writer, bizTalk.Components);
WriteHeading(writer, 2, "BizTalk-WMI-Inventar");
WriteNameValueTable(writer, bizTalk.WmiClasses);
}
/// <summary>
/// Schreibt eine ACL-Tabelle.
/// </summary>
/// <param name="writer">XML-Writer.</param>
/// <param name="rules">ACL-Regeln.</param>
private static void WriteAclTable(XmlWriter writer, IEnumerable<AccessRuleRecord> rules)
{
WriteTable(writer,
new[] { "Identität", "Rechte", "Typ", "Geerbt", "Vererbung", "Ziel" },
rules.Select(rule => new[]
{
rule.Identity, rule.Rights, rule.AccessType,
rule.IsInherited ? "Ja" : "Nein", rule.Inheritance, rule.Target
}));
}
/// <summary>
/// Schreibt eine Name/Wert-Tabelle.
/// </summary>
/// <param name="writer">XML-Writer.</param>
/// <param name="items">Name/Wert-Datensätze.</param>
private static void WriteNameValueTable(XmlWriter writer, IEnumerable<NameValueRecord> items)
{
WriteTable(writer,
new[] { "Eigenschaft", "Wert" },
items.Select(item => new[] { item.Name, item.Value }));
}
/// <summary>
/// Schreibt eine Word-Tabelle und wiederholt die Kopfzeile auf Folgeseiten.
/// </summary>
/// <param name="writer">XML-Writer.</param>
/// <param name="headers">Spaltenüberschriften.</param>
/// <param name="rows">Tabellenzeilen.</param>
private static void WriteTable(XmlWriter writer, string[] headers, IEnumerable<string[]> rows)
{
var materialized = rows == null ? new List<string[]>() : rows.ToList();
if (materialized.Count == 0)
{
WriteParagraph(writer, "Keine Daten erfasst.", "Normal");
return;
}
writer.WriteStartElement("w", "tbl", WordNamespace);
writer.WriteStartElement("w", "tblPr", WordNamespace);
writer.WriteStartElement("w", "tblStyle", WordNamespace);
writer.WriteAttributeString("w", "val", WordNamespace, "InventoryTable");
writer.WriteEndElement();
writer.WriteStartElement("w", "tblW", WordNamespace);
writer.WriteAttributeString("w", "w", WordNamespace, "0");
writer.WriteAttributeString("w", "type", WordNamespace, "auto");
writer.WriteEndElement();
writer.WriteStartElement("w", "tblLayout", WordNamespace);
writer.WriteAttributeString("w", "type", WordNamespace, "autofit");
writer.WriteEndElement();
writer.WriteEndElement();
WriteTableRow(writer, headers, true);
foreach (var row in materialized)
{
var normalized = new string[headers.Length];
for (var index = 0; index < headers.Length; index++)
{
normalized[index] = index < row.Length ? row[index] : string.Empty;
}
WriteTableRow(writer, normalized, false);
}
writer.WriteEndElement();
WriteParagraph(writer, string.Empty, "Normal");
}
/// <summary>
/// Schreibt eine Tabellenzeile.
/// </summary>
/// <param name="writer">XML-Writer.</param>
/// <param name="values">Zellwerte.</param>
/// <param name="header">Gibt eine wiederholbare Kopfzeile an.</param>
private static void WriteTableRow(XmlWriter writer, IEnumerable<string> values, bool header)
{
writer.WriteStartElement("w", "tr", WordNamespace);
if (header)
{
writer.WriteStartElement("w", "trPr", WordNamespace);
writer.WriteElementString("w", "tblHeader", WordNamespace, string.Empty);
writer.WriteEndElement();
}
foreach (var value in values)
{
writer.WriteStartElement("w", "tc", WordNamespace);
writer.WriteStartElement("w", "tcPr", WordNamespace);
if (header)
{
writer.WriteStartElement("w", "shd", WordNamespace);
writer.WriteAttributeString("w", "fill", WordNamespace, "DCEEF7");
writer.WriteEndElement();
}
writer.WriteEndElement();
WriteParagraphWithRun(writer, LimitCellText(value), header);
writer.WriteEndElement();
}
writer.WriteEndElement();
}
/// <summary>
/// Schreibt eine Überschrift mit Word-Navigationsebene.
/// </summary>
/// <param name="writer">XML-Writer.</param>
/// <param name="level">Ebene eins bis drei.</param>
/// <param name="text">Überschriftstext.</param>
private static void WriteHeading(XmlWriter writer, int level, string text)
{
WriteParagraph(writer, text, "Heading" + Math.Max(1, Math.Min(3, level)));
}
/// <summary>
/// Schreibt einen hervorgehobenen Hinweis.
/// </summary>
/// <param name="writer">XML-Writer.</param>
/// <param name="text">Hinweistext.</param>
private static void WriteNotice(XmlWriter writer, string text)
{
writer.WriteStartElement("w", "p", WordNamespace);
writer.WriteStartElement("w", "pPr", WordNamespace);
writer.WriteStartElement("w", "shd", WordNamespace);
writer.WriteAttributeString("w", "fill", WordNamespace, "FFF2CC");
writer.WriteEndElement();
writer.WriteStartElement("w", "spacing", WordNamespace);
writer.WriteAttributeString("w", "before", WordNamespace, "120");
writer.WriteAttributeString("w", "after", WordNamespace, "160");
writer.WriteEndElement();
writer.WriteEndElement();
WriteRun(writer, text, true);
writer.WriteEndElement();
}
/// <summary>
/// Schreibt einen Absatz in einer benannten Formatvorlage.
/// </summary>
/// <param name="writer">XML-Writer.</param>
/// <param name="text">Absatztext.</param>
/// <param name="style">Word-Style-ID.</param>
private static void WriteParagraph(XmlWriter writer, string text, string style)
{
writer.WriteStartElement("w", "p", WordNamespace);
writer.WriteStartElement("w", "pPr", WordNamespace);
writer.WriteStartElement("w", "pStyle", WordNamespace);
writer.WriteAttributeString("w", "val", WordNamespace, style);
writer.WriteEndElement();
writer.WriteEndElement();
WriteRun(writer, text, false);
writer.WriteEndElement();
}
/// <summary>
/// Schreibt einen einfachen Tabellenzellenabsatz.
/// </summary>
/// <param name="writer">XML-Writer.</param>
/// <param name="text">Zelltext.</param>
/// <param name="bold">Fettdruck.</param>
private static void WriteParagraphWithRun(XmlWriter writer, string text, bool bold)
{
writer.WriteStartElement("w", "p", WordNamespace);
WriteRun(writer, text, bold);
writer.WriteEndElement();
}
/// <summary>
/// Schreibt einen Textlauf und bewahrt Leerzeichen.
/// </summary>
/// <param name="writer">XML-Writer.</param>
/// <param name="text">Unvertrauenswürdiger Text; der XML-Writer kodiert ihn.</param>
/// <param name="bold">Fettdruck.</param>
private static void WriteRun(XmlWriter writer, string text, bool bold)
{
writer.WriteStartElement("w", "r", WordNamespace);
if (bold)
{
writer.WriteStartElement("w", "rPr", WordNamespace);
writer.WriteElementString("w", "b", WordNamespace, string.Empty);
writer.WriteEndElement();
}
writer.WriteStartElement("w", "t", WordNamespace);
writer.WriteAttributeString("xml", "space", "http://www.w3.org/XML/1998/namespace", "preserve");
writer.WriteString(NormalizeText(text));
writer.WriteEndElement();
writer.WriteEndElement();
}
/// <summary>
/// Schreibt Seitenformat und Ränder; Querformat verbessert breite Inventartabellen.
/// </summary>
/// <param name="writer">XML-Writer.</param>
private static void WriteSectionProperties(XmlWriter writer)
{
writer.WriteStartElement("w", "sectPr", WordNamespace);
writer.WriteStartElement("w", "pgSz", WordNamespace);
writer.WriteAttributeString("w", "w", WordNamespace, "16838");
writer.WriteAttributeString("w", "h", WordNamespace, "11906");
writer.WriteAttributeString("w", "orient", WordNamespace, "landscape");
writer.WriteEndElement();
writer.WriteStartElement("w", "pgMar", WordNamespace);
writer.WriteAttributeString("w", "top", WordNamespace, "900");
writer.WriteAttributeString("w", "right", WordNamespace, "720");
writer.WriteAttributeString("w", "bottom", WordNamespace, "900");
writer.WriteAttributeString("w", "left", WordNamespace, "720");
writer.WriteAttributeString("w", "header", WordNamespace, "360");
writer.WriteAttributeString("w", "footer", WordNamespace, "360");
writer.WriteAttributeString("w", "gutter", WordNamespace, "0");
writer.WriteEndElement();
writer.WriteEndElement();
}
/// <summary>
/// Erstellt einen komprimierten XML-Part mit sicheren Writer-Einstellungen.
/// </summary>
/// <param name="archive">Geöffnetes DOCX-ZIP-Archiv.</param>
/// <param name="path">Partpfad innerhalb des Archivs.</param>
/// <param name="writeAction">Aktion zum Schreiben des XML-Inhalts.</param>
private static void WriteXmlEntry(ZipArchive archive, string path, Action<XmlWriter> writeAction)
{
var entry = archive.CreateEntry(path, CompressionLevel.Optimal);
using (var stream = entry.Open())
using (var writer = XmlWriter.Create(stream, new XmlWriterSettings
{
Encoding = new UTF8Encoding(false),
Indent = false,
CloseOutput = false,
CheckCharacters = true
}))
{
writer.WriteStartDocument(true);
writeAction(writer);
writer.WriteEndDocument();
}
}
/// <summary>
/// Entfernt Zeichen, die in XML 1.0 nicht zulässig sind.
/// </summary>
/// <param name="value">Eingabetext.</param>
/// <returns>XML-kompatibler Text.</returns>
private static string NormalizeText(string value)
{
if (string.IsNullOrEmpty(value))
{
return string.Empty;
}
var builder = new StringBuilder(value.Length);
foreach (var character in value)
{
if (XmlConvert.IsXmlChar(character))
{
builder.Append(character);
}
else
{
builder.Append('\uFFFD');
}
}
return builder.ToString();
}
/// <summary>
/// Begrenzt Zelltext auf einen Word-kompatiblen und bedienbaren Umfang.
/// </summary>
/// <param name="value">Zelltext.</param>
/// <returns>Unveränderter oder gekürzter Text.</returns>
private static string LimitCellText(string value)
{
const int maximum = 30000;
var normalized = NormalizeText(value);
return normalized.Length <= maximum
? normalized
: normalized.Substring(0, maximum) + " … [GEKÜRZT]";
}
/// <summary>
/// Formatiert UTC-Zeit konsistent.
/// </summary>
/// <param name="value">Zeitwert.</param>
/// <returns>UTC-Darstellung.</returns>
private static string FormatUtc(DateTime value)
{
return value.ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ss 'UTC'", CultureInfo.InvariantCulture);
}
/// <summary>
/// Formatiert eine Bytezahl lesbar.
/// </summary>
/// <param name="bytes">Bytezahl.</param>
/// <returns>Lesbare Größenangabe.</returns>
private static string FormatBytes(long bytes)
{
var suffixes = new[] { "B", "KiB", "MiB", "GiB", "TiB" };
double value = bytes;
var index = 0;
while (value >= 1024 && index < suffixes.Length - 1)
{
value /= 1024;
index++;
}
return value.ToString(index == 0 ? "N0" : "N2",
CultureInfo.GetCultureInfo("de-DE")) + " " + suffixes[index];
}
/// <summary>
/// Liefert eine Sortierzahl für Findings.
/// </summary>
/// <param name="severity">Schweregrad.</param>
/// <returns>Kleinere Zahl für höhere Priorität.</returns>
private static int SeverityOrder(string severity)
{
if (string.Equals(severity, "Fehler", StringComparison.OrdinalIgnoreCase))
{
return 0;
}
return string.Equals(severity, "Warnung", StringComparison.OrdinalIgnoreCase) ? 1 : 2;
}
}
}