353 lines
14 KiB
C#
353 lines
14 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// Abhängigkeitsfreier Konsolen-Testläufer für zentrale Sicherheits- und Parserfunktionen.
|
|
/// </summary>
|
|
internal static class Program
|
|
{
|
|
private static int failures;
|
|
|
|
/// <summary>
|
|
/// Führt alle Tests aus.
|
|
/// </summary>
|
|
/// <param name="args">Optional <c>--write-sample PFAD</c> für ein DOCX-Prüfdokument.</param>
|
|
/// <returns>0 bei Erfolg, sonst 1.</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Erzeugt ein dauerhaftes minimales DOCX zur Validierung mit externen Office-Programmen.
|
|
/// </summary>
|
|
/// <param name="path">Zielpfad des Prüfdokuments.</param>
|
|
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));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Führt einen einzelnen Test isoliert aus.
|
|
/// </summary>
|
|
/// <param name="name">Testname.</param>
|
|
/// <param name="test">Testaktion.</param>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Prüft Optionsparser und sichere Dateinamensbildung.
|
|
/// </summary>
|
|
private static void TestCommandLine()
|
|
{
|
|
var options = CommandLineOptions.Parse(new[] { "--environment", " acc / 01 " });
|
|
Equal("ACC-01", options.EnvironmentName);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Prüft Attribut- und Ciphertext-Bereinigung.
|
|
/// </summary>
|
|
private static void TestSanitizer()
|
|
{
|
|
var source = XElement.Parse(
|
|
"<configuration password=\"klartext\"><add token=\"abc\"/><EncryptedData><CipherValue>blob</CipherValue></EncryptedData></configuration>");
|
|
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
|
|
/// <summary>
|
|
/// Prüft IIS-Topologie und Secret-freien Konfigurationshash anhand einer Testdatei.
|
|
/// </summary>
|
|
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 = @"<configuration>
|
|
<configSections>
|
|
<sectionGroup name=""system.webServer""><section name=""security"" overrideModeDefault=""Allow"" /></sectionGroup>
|
|
</configSections>
|
|
<configProtectedData><providers><add name=""IISWASOnlyRsaProvider"" type=""Provider"" keyContainerName=""iisWasKey"" useMachineContainer=""true"" /></providers></configProtectedData>
|
|
<system.applicationHost>
|
|
<applicationPools><add name=""BizTalkPool"" managedRuntimeVersion=""v4.0""><processModel identityType=""SpecificUser"" userName=""DOMAIN\svc"" password=""secret"" /></add></applicationPools>
|
|
<sites><site name=""BizTalk"" id=""1""><application path=""/"" applicationPool=""BizTalkPool""><virtualDirectory path=""/"" physicalPath=""" + EscapeXml(webRoot) + @""" /></application><bindings><binding protocol=""https"" bindingInformation=""*:443:test"" certificateHash=""AA BB"" certificateStoreName=""MY"" /></bindings></site></sites>
|
|
</system.applicationHost>
|
|
<system.webServer><security /></system.webServer>
|
|
</configuration>";
|
|
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<Finding>();
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Prüft das Parsen eines minimalen secedit-Exports.
|
|
/// </summary>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Prüft erforderliche Office-Open-XML-Parts, XML-Validität und sichere Textkodierung.
|
|
/// </summary>
|
|
private static void TestDocxGeneration()
|
|
{
|
|
var path = Path.Combine(
|
|
Path.GetTempPath(),
|
|
"BizTalkIisInventory-DocxTest-" + Guid.NewGuid().ToString("N") + ".docx");
|
|
var document = new InventoryDocument
|
|
{
|
|
EnvironmentName = "<script>alert(1)</script>",
|
|
ComputerName = "TEST&HOST",
|
|
StartedUtc = DateTime.UtcNow,
|
|
CompletedUtc = DateTime.UtcNow
|
|
};
|
|
document.System.Properties.Add(new NameValueRecord("Test", "<b>nicht fett</b>"));
|
|
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("<script>alert(1)</script>"),
|
|
"Umgebungsname wurde als Markup statt Text geschrieben.");
|
|
False(xml.Contains("<b>nicht fett</b>"),
|
|
"Tabellenwert wurde als Markup statt Text geschrieben.");
|
|
True(xml.Contains("Schutz sensibler Daten"),
|
|
"Sicherheitskennzeichnung fehlt.");
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (File.Exists(path))
|
|
{
|
|
File.Delete(path);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Prüft, dass OPC-Elemente im vorgeschriebenen Namespace statt im leeren Namespace liegen.
|
|
/// </summary>
|
|
/// <param name="archive">Geöffnetes DOCX-Paket.</param>
|
|
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.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Lädt einen XML-Part aus dem DOCX-Paket.
|
|
/// </summary>
|
|
/// <param name="archive">Geöffnetes DOCX-Paket.</param>
|
|
/// <param name="entryName">Pfad des XML-Parts.</param>
|
|
/// <returns>Geparstes XML-Dokument.</returns>
|
|
private static XDocument LoadEntryXml(ZipArchive archive, string entryName)
|
|
{
|
|
using (var stream = archive.GetEntry(entryName).Open())
|
|
{
|
|
return XDocument.Load(stream);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Erstellt ein eindeutiges temporäres Testverzeichnis.
|
|
/// </summary>
|
|
/// <returns>Vollständiger Pfad.</returns>
|
|
private static string CreateTemporaryDirectory()
|
|
{
|
|
var path = Path.Combine(Path.GetTempPath(), "BizTalkIisInventoryTests-" + Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(path);
|
|
return path;
|
|
}
|
|
|
|
/// <summary>
|
|
/// XML-kodiert einen Attributwert.
|
|
/// </summary>
|
|
/// <param name="value">Unkodierter Wert.</param>
|
|
/// <returns>XML-kodierter Wert.</returns>
|
|
private static string EscapeXml(string value)
|
|
{
|
|
return new XAttribute("x", value).ToString()
|
|
.Substring(3)
|
|
.TrimEnd('\"');
|
|
}
|
|
|
|
/// <summary>
|
|
/// Vergleicht zwei Werte.
|
|
/// </summary>
|
|
/// <typeparam name="T">Werttyp.</typeparam>
|
|
/// <param name="expected">Erwarteter Wert.</param>
|
|
/// <param name="actual">Tatsächlicher Wert.</param>
|
|
private static void Equal<T>(T expected, T actual)
|
|
{
|
|
if (!EqualityComparer<T>.Default.Equals(expected, actual))
|
|
{
|
|
throw new InvalidOperationException("Erwartet: " + expected + "; tatsächlich: " + actual);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Erwartet einen wahren Ausdruck.
|
|
/// </summary>
|
|
/// <param name="condition">Zu prüfender Ausdruck.</param>
|
|
/// <param name="message">Fehlermeldung.</param>
|
|
private static void True(bool condition, string message)
|
|
{
|
|
if (!condition)
|
|
{
|
|
throw new InvalidOperationException(message);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Erwartet einen falschen Ausdruck.
|
|
/// </summary>
|
|
/// <param name="condition">Zu prüfender Ausdruck.</param>
|
|
/// <param name="message">Fehlermeldung.</param>
|
|
private static void False(bool condition, string message)
|
|
{
|
|
True(!condition, message);
|
|
}
|
|
}
|
|
}
|