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""><binding name=""sapBinding"" useSnc=""true"" sncLibrary=""C:\SAP\sapcrypto.dll"" sncPartnerName=""p:SAP/ERP"" /></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; }
}
}
}