using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; using System.Text; using System.Xml.Linq; #if NETFRAMEWORK using BizTalkIisEnvironmentInventory.Collectors; using BizTalkIisEnvironmentInventory.Configuration; #endif using BizTalkIisEnvironmentInventory.Infrastructure; using BizTalkIisEnvironmentInventory.Models; using BizTalkIisEnvironmentInventory.Reporting; namespace BizTalkIisEnvironmentInventory.Tests { /// /// Abhängigkeitsfreier Konsolen-Testläufer für zentrale Sicherheits- und Parserfunktionen. /// internal static class Program { private static int failures; /// /// Führt alle Tests aus. /// /// Optional --write-sample PFAD für ein DOCX-Prüfdokument. /// 0 bei Erfolg, sonst 1. private static int Main(string[] args) { if (args.Length == 2 && string.Equals(args[0], "--write-sample", StringComparison.OrdinalIgnoreCase)) { WriteSampleDocument(args[1]); Console.WriteLine("DOCX-Prüfdokument geschrieben: " + Path.GetFullPath(args[1])); return 0; } Run("Kommandozeile normalisiert Umgebung", TestCommandLine); Run("XML-Sanitizer entfernt Secrets", TestSanitizer); #if NETFRAMEWORK Run("IIS-Parser liest Topologie", TestIisParser); Run("Security-Policy-Parser liest User Rights", TestSecurityPolicyParser); #endif Run("DOCX-Paket ist valide und XML-sicher", TestDocxGeneration); Console.WriteLine(failures == 0 ? "Alle Tests erfolgreich." : failures + " Test(s) fehlgeschlagen."); return failures == 0 ? 0 : 1; } /// /// Erzeugt ein dauerhaftes minimales DOCX zur Validierung mit externen Office-Programmen. /// /// Zielpfad des Prüfdokuments. private static void WriteSampleDocument(string path) { var document = new InventoryDocument { EnvironmentName = "ACC-TEST", ComputerName = "BIZTALK-TEST", StartedUtc = DateTime.UtcNow.AddSeconds(-2), CompletedUtc = DateTime.UtcNow }; document.System.Properties.Add(new NameValueRecord("Betriebssystem", "Windows Server 2019 Datacenter")); document.SectionStatuses.Add(new SectionStatus { Name = "DOCX-Test", Status = "Erfolgreich", Message = "Prüfdokument", DurationMilliseconds = 12 }); new DocxReportWriter().Write(document, Path.GetFullPath(path)); } /// /// Führt einen einzelnen Test isoliert aus. /// /// Testname. /// Testaktion. private static void Run(string name, Action test) { try { test(); Console.WriteLine("[OK] " + name); } catch (Exception exception) { failures++; Console.Error.WriteLine("[FEHLER] " + name + ": " + exception.Message); } } /// /// Prüft Optionsparser und sichere Dateinamensbildung. /// private static void TestCommandLine() { var options = CommandLineOptions.Parse(new[] { "--environment", " acc / 01 " }); Equal("ACC-01", options.EnvironmentName); } /// /// Prüft Attribut- und Ciphertext-Bereinigung. /// private static void TestSanitizer() { var source = XElement.Parse( "blob"); var result = SensitiveDataSanitizer.SanitizeXml(source).ToString(); False(result.Contains("klartext"), "Kennwort blieb erhalten."); False(result.Contains("abc"), "Token blieb erhalten."); False(result.Contains("blob"), "Ciphertext blieb erhalten."); } #if NETFRAMEWORK /// /// Prüft IIS-Topologie und Secret-freien Konfigurationshash anhand einer Testdatei. /// private static void TestIisParser() { var directory = CreateTemporaryDirectory(); try { var webRoot = Path.Combine(directory, "web"); Directory.CreateDirectory(webRoot); File.WriteAllText(Path.Combine(webRoot, "default.htm"), "test", Encoding.UTF8); var configPath = Path.Combine(directory, "applicationHost.config"); var xml = @"
"; File.WriteAllText(configPath, xml, Encoding.UTF8); var commandLine = CommandLineOptions.Parse(new[] { "--environment", "TEST", "--iis-config", configPath }); var options = CollectorOptions.Load(commandLine); var target = new IisInventory(); var findings = new List(); new IisCollector(options).Collect(target, findings, configPath); Equal(1, target.ApplicationPools.Count); Equal(@"DOMAIN\svc", target.ApplicationPools[0].UserName); Equal(1, target.Sites.Count); Equal("AABB", target.Sites[0].Bindings[0].CertificateHash); Equal(1, target.Sites[0].Applications[0].TotalFiles); True(!string.IsNullOrWhiteSpace(target.SanitizedConfigurationSha256), "Konfigurationshash fehlt."); } finally { Directory.Delete(directory, true); } } /// /// Prüft das Parsen eines minimalen secedit-Exports. /// private static void TestSecurityPolicyParser() { var directory = CreateTemporaryDirectory(); try { var path = Path.Combine(directory, "security.inf"); File.WriteAllText( path, "[Unicode]\r\nUnicode=yes\r\n[Privilege Rights]\r\nSeServiceLogonRight = *S-1-5-20,DOMAIN\\svc\r\n[System Access]\r\nMinimumPasswordAge = 1\r\n", Encoding.Unicode); var target = new SecurityInventory(); SecurityCollector.ParseSecurityPolicy(path, target); Equal(1, target.UserRights.Count); Equal("SeServiceLogonRight", target.UserRights[0].Right); Equal(1, target.LocalPolicy.Count); } finally { Directory.Delete(directory, true); } } #endif /// /// Prüft erforderliche Office-Open-XML-Parts, XML-Validität und sichere Textkodierung. /// private static void TestDocxGeneration() { var path = Path.Combine( Path.GetTempPath(), "BizTalkIisInventory-DocxTest-" + Guid.NewGuid().ToString("N") + ".docx"); var document = new InventoryDocument { EnvironmentName = "", ComputerName = "TEST&HOST", StartedUtc = DateTime.UtcNow, CompletedUtc = DateTime.UtcNow }; document.System.Properties.Add(new NameValueRecord("Test", "nicht fett")); try { new DocxReportWriter().Write(document, path); using (var archive = ZipFile.OpenRead(path)) { var required = new[] { "[Content_Types].xml", "_rels/.rels", "word/document.xml", "word/styles.xml", "word/_rels/document.xml.rels", "docProps/core.xml", "docProps/app.xml" }; foreach (var entryName in required) { True(archive.GetEntry(entryName) != null, "DOCX-Part fehlt: " + entryName); } foreach (var entry in archive.Entries.Where(item => item.FullName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase) || item.FullName.EndsWith(".rels", StringComparison.OrdinalIgnoreCase))) { using (var stream = entry.Open()) { XDocument.Load(stream); } } AssertOpenXmlNamespaces(archive); var documentEntry = archive.GetEntry("word/document.xml"); using (var stream = documentEntry.Open()) using (var reader = new StreamReader(stream, Encoding.UTF8)) { var xml = reader.ReadToEnd(); False(xml.Contains(""), "Umgebungsname wurde als Markup statt Text geschrieben."); False(xml.Contains("nicht fett"), "Tabellenwert wurde als Markup statt Text geschrieben."); True(xml.Contains("Schutz sensibler Daten"), "Sicherheitskennzeichnung fehlt."); } } } finally { if (File.Exists(path)) { File.Delete(path); } } } /// /// Prüft, dass OPC-Elemente im vorgeschriebenen Namespace statt im leeren Namespace liegen. /// /// Geöffnetes DOCX-Paket. private static void AssertOpenXmlNamespaces(ZipArchive archive) { var contentTypes = LoadEntryXml(archive, "[Content_Types].xml"); var contentTypeNamespace = (XNamespace) "http://schemas.openxmlformats.org/package/2006/content-types"; True(contentTypes.Root.Elements(contentTypeNamespace + "Override").Any(), "Content-Type-Overrides liegen nicht im OPC-Namespace."); var relationships = LoadEntryXml(archive, "_rels/.rels"); var relationshipNamespace = (XNamespace) "http://schemas.openxmlformats.org/package/2006/relationships"; True(relationships.Root.Elements(relationshipNamespace + "Relationship").Count() == 3, "Paketbeziehungen liegen nicht vollständig im OPC-Namespace."); } /// /// Lädt einen XML-Part aus dem DOCX-Paket. /// /// Geöffnetes DOCX-Paket. /// Pfad des XML-Parts. /// Geparstes XML-Dokument. private static XDocument LoadEntryXml(ZipArchive archive, string entryName) { using (var stream = archive.GetEntry(entryName).Open()) { return XDocument.Load(stream); } } /// /// Erstellt ein eindeutiges temporäres Testverzeichnis. /// /// Vollständiger Pfad. private static string CreateTemporaryDirectory() { var path = Path.Combine(Path.GetTempPath(), "BizTalkIisInventoryTests-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(path); return path; } /// /// XML-kodiert einen Attributwert. /// /// Unkodierter Wert. /// XML-kodierter Wert. private static string EscapeXml(string value) { return new XAttribute("x", value).ToString() .Substring(3) .TrimEnd('\"'); } /// /// Vergleicht zwei Werte. /// /// Werttyp. /// Erwarteter Wert. /// Tatsächlicher Wert. private static void Equal(T expected, T actual) { if (!EqualityComparer.Default.Equals(expected, actual)) { throw new InvalidOperationException("Erwartet: " + expected + "; tatsächlich: " + actual); } } /// /// Erwartet einen wahren Ausdruck. /// /// Zu prüfender Ausdruck. /// Fehlermeldung. private static void True(bool condition, string message) { if (!condition) { throw new InvalidOperationException(message); } } /// /// Erwartet einen falschen Ausdruck. /// /// Zu prüfender Ausdruck. /// Fehlermeldung. private static void False(bool condition, string message) { True(!condition, message); } } }