513 lines
20 KiB
C#
513 lines
20 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// Abhängigkeitsfreier Testläufer für Secret-Schutz, SAP-Bindingparser und DOCX-Paket.
|
|
/// </summary>
|
|
internal static class SelfTestRunner
|
|
{
|
|
/// <summary>
|
|
/// Führt alle sicherheits- und ausgaberelevanten Tests aus.
|
|
/// </summary>
|
|
public static void Run()
|
|
{
|
|
TestSanitizer();
|
|
TestBindingParser();
|
|
TestExplorerArtifactReader();
|
|
TestLoggerDiagnostics();
|
|
TestDocxPackage();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Schreibt einen dauerhaften Musterbericht für eine externe Word-/LibreOffice-Prüfung.
|
|
/// </summary>
|
|
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(
|
|
"<configuration password=\"clear\"><add token=\"abc\" />"
|
|
+ "<EncryptedData><CipherValue>blob</CipherValue></EncryptedData></configuration>");
|
|
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(
|
|
"<binding password=\"xmlsecret\" token='xmltoken' />");
|
|
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 = "<ACC&TEST>",
|
|
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<Send>",
|
|
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("<ACC&TEST>"), "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(
|
|
@"<BindingInfo>
|
|
<ModuleRefCollection>
|
|
<ModuleRef Name=""[Application:SAP_Order]"" />
|
|
</ModuleRefCollection>
|
|
<SendPortCollection>
|
|
<SendPort Name=""Send_SAP_Order"">
|
|
<ApplicationName>SAP_Order</ApplicationName>
|
|
<PrimaryTransport>
|
|
<Address>sap://Client=100;lang=DE@A/sap.example.local/00?GwHost=gateway.example.local&GwServ=sapgw00</Address>
|
|
<TransportType Name=""WCF-SAP"" />
|
|
<SendHandler Name=""Snd_SAP_ERP_C1_64_B"" />
|
|
<TransportTypeData><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></TransportTypeData>
|
|
</PrimaryTransport>
|
|
</SendPort>
|
|
</SendPortCollection>
|
|
<ReceivePortCollection>
|
|
<ReceivePort Name=""Receive_SAP_Order"">
|
|
<ApplicationName>SAP_Order</ApplicationName>
|
|
<ReceiveLocations>
|
|
<ReceiveLocation Name=""ReceiveLocation_SAP_Order"" Enable=""true"">
|
|
<Address>sap://Client=100;lang=DE@D/RFC_TEST?ListenerGwHost=gateway.example.local&ListenerGwServ=sapgw00&ListenerProgramId=BIZTALK_TEST</Address>
|
|
<ReceiveLocationTransportType Name=""WCF-Custom"" />
|
|
<ReceiveHandler Name=""Rcv_SAP_ERP_C1_64_B"" />
|
|
<ReceiveLocationTransportTypeData>
|
|
<CustomProps>
|
|
<BindingType vt=""8"">sapBinding</BindingType>
|
|
<BindingConfiguration vt=""8""><binding name=""sapBinding"" useSnc=""true"" sncLibrary=""C:\SAP\sapcrypto.dll"" sncPartnerName=""p:SAP/ERP"" /></BindingConfiguration>
|
|
</CustomProps>
|
|
</ReceiveLocationTransportTypeData>
|
|
</ReceiveLocation>
|
|
</ReceiveLocations>
|
|
</ReceivePort>
|
|
</ReceivePortCollection>
|
|
</BindingInfo>");
|
|
}
|
|
|
|
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; }
|
|
}
|
|
}
|
|
}
|