using System; using System.Collections; using System.IO; using System.IO.Compression; using System.Linq; using System.Reflection; using System.Text; using System.Xml.Linq; using BizTalkSapEnvironmentInventory.Collectors; using BizTalkSapEnvironmentInventory.Configuration; using BizTalkSapEnvironmentInventory.Models; using BizTalkSapEnvironmentInventory.Reporting; namespace BizTalkSapEnvironmentInventory.Infrastructure { /// /// Abhängigkeitsfreier Testläufer für Secret-Schutz, SAP-Bindingparser und DOCX-Paket. /// internal static class SelfTestRunner { /// /// Führt alle sicherheits- und ausgaberelevanten Tests aus. /// public static void Run() { TestSanitizer(); TestBindingParser(); TestExplorerArtifactReader(); TestLoggerDiagnostics(); TestDocxPackage(); } /// /// Schreibt einen dauerhaften Musterbericht für eine externe Word-/LibreOffice-Prüfung. /// public static void WriteSample(string path) { var document = new InventoryDocument { EnvironmentName = "ACC-MUSTER", ComputerName = "BIZTALK-ACC-01", ToolVersion = "1.0.0.0", StartedUtc = DateTime.UtcNow.AddSeconds(-3), CompletedUtc = DateTime.UtcNow }; document.System.Properties.Add( new NameValueRecord("BizTalk Produktversion", "3.13.x (Muster)")); document.System.Properties.Add( new NameValueRecord("BizTalk Management SQL Server", "SQL-ACC-01")); document.Applications.Add(new ApplicationRecord { Name = "MasterData_SAP_WebGIS", Status = "Gestartet" }); var endpoint = new SapEndpointRecord { ApplicationName = "MasterData_SAP_WebGIS", Direction = "Senden", Name = "Send_SAP_WebGIS", ParentPortName = "Send_SAP_WebGIS", AdapterName = "WCF-SAP", HostName = "Snd_SAP_ERP_C1_64_B", Status = "Gestartet", Address = "sap://Client=100;lang=DE@A/sap-acc.example.local/00", SecurityMode = "SAP SNC", CredentialReference = "Kennwort wird nicht dokumentiert.", Source = "Muster" }; endpoint.Properties.Add(new NameValueRecord("Client", "100", "SAP-URI")); endpoint.Properties.Add(new NameValueRecord("GatewayHost", "sap-gw-acc", "SAP-URI")); endpoint.Properties.Add(new NameValueRecord("UseSnc", "true", "Binding")); endpoint.Properties.Add(new NameValueRecord("SncLibrary", @"C:\SAP\sapcrypto.dll", "Binding")); endpoint.Operations.Add("http://Microsoft.LobServices.Sap/2007/03/Idoc/3/ORDERS05"); document.SapEndpoints.Add(endpoint); document.SectionStatuses.Add(new SectionStatus { Name = "Mustererfassung", Status = "Erfolgreich", Message = "Musterbericht", Required = true, DurationMilliseconds = 42 }); document.Findings.Add(new Finding { Severity = "Offen", Area = "SAP WE20", Message = "Partnerprofil muss aus SAP ergänzt werden.", RecommendedAction = "WE20-Export beifügen.", Owner = "SAP Basis" }); new DocxReportWriter().Write(document, Path.GetFullPath(path)); } private static void TestSanitizer() { var source = XElement.Parse( "" + "blob"); var result = SensitiveDataSanitizer.SanitizeXml(source).ToString(); Assert(!result.Contains("clear"), "XML-Sanitizer ließ Kennwort stehen."); Assert(!result.Contains("abc"), "XML-Sanitizer ließ Token stehen."); Assert(!result.Contains("blob"), "XML-Sanitizer ließ Ciphertext stehen."); var uri = SensitiveDataSanitizer.SanitizeText( "sap://Client=100;Password=secret@A/sap.example/00?token=abc"); Assert(!uri.Contains("secret"), "URI-Sanitizer ließ Kennwort stehen."); Assert(!uri.Contains("abc"), "URI-Sanitizer ließ Token stehen."); var embeddedXml = SensitiveDataSanitizer.SanitizeText( ""); Assert(!embeddedXml.Contains("xmlsecret"), "Text-Sanitizer ließ XML-Kennwort stehen."); Assert(!embeddedXml.Contains("xmltoken"), "Text-Sanitizer ließ XML-Token stehen."); } private static void TestBindingParser() { var directory = CreateTemporaryDirectory(); var logPath = Path.Combine(directory, "test.log"); try { var document = new InventoryDocument { EnvironmentName = "TEST", ComputerName = "BIZTALK-TEST", StartedUtc = DateTime.UtcNow }; var options = CommandLineOptions.Parse(new[] { "--environment", "TEST", "--output", directory }); using (var logger = new ConsoleFileLogger(logPath)) { var collector = new BindingExportCollector(options, document, logger, 30); collector.ParseBindingDocument(CreateSampleBinding(), "Self-Test"); } Assert(document.SapEndpoints.Count == 2, "Nicht alle SAP-Endpunkte wurden erkannt."); var endpoint = document.SapEndpoints.Single(item => item.Direction == "Senden"); Assert(endpoint.GetProperty("Client") == "100", "SAP Client wurde nicht aus URI gelesen."); Assert( endpoint.GetProperty("ApplicationServerHost") == "sap.example.local", "SAP Application Server wurde nicht aus URI gelesen."); Assert(endpoint.GetProperty("UseSnc") == "true", "UseSnc wurde nicht gelesen."); Assert( endpoint.Properties.All(item => !item.Value.Contains("supersecret")), "Binding-Parser ließ ein Kennwort stehen."); Assert(endpoint.SecurityMode == "SAP SNC", "SNC-Modus wurde nicht erkannt."); Assert(endpoint.ApplicationName == "SAP_Order", "Anwendungszuordnung fehlt."); var receive = document.SapEndpoints.Single(item => item.Direction == "Empfangen"); Assert(receive.AdapterName == "WCF-Custom", "WCF-Custom SAP-Endpunkt fehlt."); Assert( receive.GetProperty("ListenerProgramId") == "BIZTALK_TEST", "Listener Program ID wurde nicht aus URI gelesen."); Assert( receive.GetProperty("DestinationName") == "RFC_TEST", "Destination Name wurde nicht aus URI gelesen."); } finally { DeleteDirectory(directory); } } private static void TestDocxPackage() { var directory = CreateTemporaryDirectory(); var path = Path.Combine(directory, "self-test.docx"); try { var document = new InventoryDocument { EnvironmentName = "", ComputerName = "BIZTALK&01", ToolVersion = "1.0.0.0", StartedUtc = DateTime.UtcNow.AddSeconds(-1), CompletedUtc = DateTime.UtcNow }; document.Applications.Add(new ApplicationRecord { Name = "SAP_Order", Status = "Gestartet" }); document.SapEndpoints.Add(new SapEndpointRecord { ApplicationName = "SAP_Order", Direction = "Senden", Name = "SAP", AdapterName = "WCF-SAP", Address = "sap://Client=100@A/sap.example.local/00", SecurityMode = "SAP SNC", CredentialReference = "Kein Kennwort", Source = "Self-Test" }); document.SapEndpoints[0].Properties.Add( new NameValueRecord("Password", "supersecret", "Self-Test")); new DocxReportWriter().Write(document, path); using (var archive = ZipFile.OpenRead(path)) { foreach (var required in new[] { "[Content_Types].xml", "_rels/.rels", "word/document.xml", "word/styles.xml", "word/_rels/document.xml.rels", "docProps/core.xml", "docProps/app.xml" }) { Assert(archive.GetEntry(required) != null, "DOCX-Part fehlt: " + required); } 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); } } AssertPackageNamespaces(archive); using (var stream = archive.GetEntry("word/document.xml").Open()) using (var reader = new StreamReader(stream, Encoding.UTF8)) { var xml = reader.ReadToEnd(); Assert(!xml.Contains("supersecret"), "DOCX enthält Testkennwort."); Assert(!xml.Contains(""), "Text wurde nicht XML-sicher geschrieben."); } } } finally { DeleteDirectory(directory); } } private static void TestExplorerArtifactReader() { var directory = CreateTemporaryDirectory(); var logPath = Path.Combine(directory, "explorer-artifacts.log"); try { var document = new InventoryDocument(); var application = new ApplicationRecord { Name = "ExplorerOM-Test", Status = "Gestartet" }; using (var logger = new ConsoleFileLogger(logPath)) { var collector = BizTalkWmiCollector.CreateForExplorerArtifactTest( document, logger, 5000); collector.CollectExplorerArtifactsForTest( new FakeApplication(new FakeAssembly( "Test.Assembly, Version=1.0.0.0", new FakeArtifact("Test.Schema"), new FakeArtifact("Test.Map"), new FakeArtifact("Test.Pipeline"))), application); } Assert(application.Count("Assembly") == 1, "ExplorerOM-Assembly wurde nicht gelesen."); Assert(application.Count("Schema") == 1, "ExplorerOM-Schema wurde nicht gelesen."); Assert(application.Count("Map") == 1, "ExplorerOM-Map wurde nicht gelesen."); Assert(application.Count("Pipeline") == 1, "ExplorerOM-Pipeline wurde nicht gelesen."); Assert( application.Artifacts.All(item => item.Properties.Any(property => property.Value == "BizTalk Explorer Object Model")), "ExplorerOM-Quelle fehlt an einem Artefakt."); } finally { DeleteDirectory(directory); } } private static void TestLoggerDiagnostics() { var directory = CreateTemporaryDirectory(); var path = Path.Combine(directory, "diagnostics.log"); try { var exception = new TargetInvocationException( new InvalidOperationException("Innerer Diagnosefehler")); using (var logger = new ConsoleFileLogger(path)) { var originalOutput = Console.Out; try { // Die absichtlich erzeugte Testausnahme soll den Self-Test // auf der Konsole nicht wie einen echten Laufzeitfehler aussehen lassen. Console.SetOut(TextWriter.Null); logger.WarningException("Self-Test-Diagnose", exception); } finally { Console.SetOut(originalOutput); } } var log = File.ReadAllText(path); Assert( log.Contains(typeof(TargetInvocationException).FullName), "Logger dokumentierte die Reflection-Ausnahme nicht."); Assert( log.Contains(typeof(InvalidOperationException).FullName), "Logger dokumentierte die innere Ausnahme nicht."); Assert( log.Contains("HRESULT=0x"), "Logger dokumentierte den HRESULT nicht."); Assert( log.Contains("[WARNUNG] Self-Test-Diagnose") && !log.Contains("[FEHLER] Self-Test-Diagnose"), "Optionale Ausnahme wurde nicht als Warnung protokolliert."); } finally { DeleteDirectory(directory); } } private static void AssertPackageNamespaces(ZipArchive archive) { var contentTypes = LoadEntryXml(archive, "[Content_Types].xml"); var contentTypeNamespace = (XNamespace) "http://schemas.openxmlformats.org/package/2006/content-types"; Assert( 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"; Assert( relationships.Root.Elements(relationshipNamespace + "Relationship").Count() == 3, "Paketbeziehungen liegen nicht vollständig im OPC-Namespace."); } private static XDocument LoadEntryXml(ZipArchive archive, string entryName) { using (var stream = archive.GetEntry(entryName).Open()) { return XDocument.Load(stream); } } private static XDocument CreateSampleBinding() { return XDocument.Parse( @" SAP_Order
sap://Client=100;lang=DE@A/sap.example.local/00?GwHost=gateway.example.local&GwServ=sapgw00
<CustomProps> <BindingType vt=""8"">sapBinding</BindingType> <BindingConfiguration vt=""8"">&lt;binding name=""sapBinding"" useSnc=""true"" sncLibrary=""C:\SAP\sapcrypto.dll"" sncPartnerName=""p:SAP/ERP"" /&gt;</BindingConfiguration> <Password vt=""8"">supersecret</Password> <Action vt=""8"">http://Microsoft.LobServices.Sap/2007/03/Idoc/3/ORDERS05</Action> </CustomProps>
SAP_Order
sap://Client=100;lang=DE@D/RFC_TEST?ListenerGwHost=gateway.example.local&ListenerGwServ=sapgw00&ListenerProgramId=BIZTALK_TEST
sapBinding <binding name=""sapBinding"" useSnc=""true"" sncLibrary=""C:\SAP\sapcrypto.dll"" sncPartnerName=""p:SAP/ERP"" />
"); } private static string CreateTemporaryDirectory() { var path = Path.Combine( Path.GetTempPath(), "BizTalkSapInventoryTests-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(path); return path; } private static void DeleteDirectory(string path) { try { if (Directory.Exists(path)) { Directory.Delete(path, true); } } catch (IOException) { } catch (UnauthorizedAccessException) { } } private static void Assert(bool condition, string message) { if (!condition) { throw new InvalidOperationException(message); } } private interface IFakeApplication { IEnumerable Assemblies { get; } } private interface IFakeAssembly { IEnumerable Schemas { get; } IEnumerable Transforms { get; } IEnumerable Pipelines { get; } } private sealed class FakeApplication : IFakeApplication { private readonly IEnumerable assemblies; public FakeApplication(params object[] assemblies) { this.assemblies = assemblies; } // ExplorerOM stellt einige Sammlungen explizit über Interfaces bereit. IEnumerable IFakeApplication.Assemblies { get { return assemblies; } } } private sealed class FakeAssembly : IFakeAssembly { private readonly IEnumerable schemas; private readonly IEnumerable transforms; private readonly IEnumerable pipelines; public FakeAssembly( string displayName, object schema, object transform, object pipeline) { DisplayName = displayName; schemas = new[] { schema }; transforms = new[] { transform }; pipelines = new[] { pipeline }; } public string DisplayName { get; private set; } IEnumerable IFakeAssembly.Schemas { get { return schemas; } } IEnumerable IFakeAssembly.Transforms { get { return transforms; } } IEnumerable IFakeAssembly.Pipelines { get { return pipelines; } } } private sealed class FakeArtifact { public FakeArtifact(string fullName) { FullName = fullName; } public string FullName { get; private set; } } } }