Initial commit: BizTalk IIS inventory with DOCX reporting
Build und Test / build (push) Has been cancelled
Build und Test / build (push) Has been cancelled
This commit is contained in:
+28
@@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net472</TargetFrameworks>
|
||||
<!-- Auf Linux zusätzlich plattformneutrale Sicherheits-/DOCX-Tests tatsächlich ausführen. -->
|
||||
<TargetFrameworks Condition="'$(OS)' != 'Windows_NT'">net472;net10.0</TargetFrameworks>
|
||||
<RootNamespace>BizTalkIisEnvironmentInventory.Tests</RootNamespace>
|
||||
<AssemblyName>BizTalkIisEnvironmentInventory.Tests</AssemblyName>
|
||||
<LangVersion>7.3</LangVersion>
|
||||
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
|
||||
<Deterministic>true</Deterministic>
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net472'">
|
||||
<ProjectReference Include="..\..\src\BizTalkIisEnvironmentInventory\BizTalkIisEnvironmentInventory.csproj" />
|
||||
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="All" />
|
||||
<Reference Include="System.IO.Compression" />
|
||||
<Reference Include="System.IO.Compression.FileSystem" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net10.0'">
|
||||
<Compile Include="..\..\src\BizTalkIisEnvironmentInventory\Infrastructure\CommandLineOptions.cs" Link="Shared\CommandLineOptions.cs" />
|
||||
<Compile Include="..\..\src\BizTalkIisEnvironmentInventory\Infrastructure\SensitiveDataSanitizer.cs" Link="Shared\SensitiveDataSanitizer.cs" />
|
||||
<Compile Include="..\..\src\BizTalkIisEnvironmentInventory\Models\InventoryModels.cs" Link="Shared\InventoryModels.cs" />
|
||||
<Compile Include="..\..\src\BizTalkIisEnvironmentInventory\Reporting\DocxReportWriter.cs" Link="Shared\DocxReportWriter.cs" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,352 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user