Initial commit: BizTalk application catalog
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace BizTalkApplicationCatalog.Infrastructure
|
||||
{
|
||||
/// <summary>
|
||||
/// Spiegelt alle Fortschrittsmeldungen zeitgleich auf die Konsole und in eine UTF-8-Logdatei.
|
||||
/// </summary>
|
||||
internal sealed class ConsoleFileLogger : IDisposable
|
||||
{
|
||||
private readonly object sync = new object();
|
||||
private readonly StreamWriter writer;
|
||||
|
||||
public ConsoleFileLogger(string path)
|
||||
{
|
||||
writer = new StreamWriter(path, false, new UTF8Encoding(false)) { AutoFlush = true };
|
||||
}
|
||||
|
||||
public void Info(string message) { Write("INFO", message); }
|
||||
public void Warning(string message) { Write("WARNUNG", message); }
|
||||
public void Error(string message) { Write("FEHLER", message); }
|
||||
public void Dispose() { writer.Dispose(); }
|
||||
|
||||
private void Write(string level, string message)
|
||||
{
|
||||
var line = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{0:yyyy-MM-dd HH:mm:ss.fff} [{1}] {2}",
|
||||
DateTime.Now,
|
||||
level,
|
||||
message ?? string.Empty);
|
||||
lock (sync)
|
||||
{
|
||||
Console.WriteLine(line);
|
||||
writer.WriteLine(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using BizTalkApplicationCatalog.Models;
|
||||
|
||||
namespace BizTalkApplicationCatalog.Infrastructure
|
||||
{
|
||||
/// <summary>
|
||||
/// Isoliert Erfassungsfehler, protokolliert sie und lässt unabhängige Abschnitte weiterlaufen.
|
||||
/// </summary>
|
||||
internal sealed class SafeCollector
|
||||
{
|
||||
private readonly InventoryDocument document;
|
||||
private readonly ConsoleFileLogger logger;
|
||||
|
||||
public SafeCollector(InventoryDocument document, ConsoleFileLogger logger)
|
||||
{
|
||||
this.document = document;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public void Execute(string name, bool required, Action action)
|
||||
{
|
||||
var timer = Stopwatch.StartNew();
|
||||
logger.Info("Starte Abschnitt: " + name);
|
||||
try
|
||||
{
|
||||
action();
|
||||
timer.Stop();
|
||||
document.SectionStatuses.Add(new SectionStatus
|
||||
{
|
||||
Name = name,
|
||||
Status = "Erfolgreich",
|
||||
Message = "Abschnitt vollständig ausgeführt.",
|
||||
Required = required,
|
||||
DurationMilliseconds = timer.ElapsedMilliseconds
|
||||
});
|
||||
logger.Info("Abschnitt abgeschlossen: " + name + " (" + timer.ElapsedMilliseconds + " ms)");
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
timer.Stop();
|
||||
document.SectionStatuses.Add(new SectionStatus
|
||||
{
|
||||
Name = name,
|
||||
Status = required ? "Fehler" : "Teilweise",
|
||||
Message = exception.GetType().Name + ": " + exception.Message,
|
||||
Required = required,
|
||||
DurationMilliseconds = timer.ElapsedMilliseconds
|
||||
});
|
||||
document.Findings.Add(new Finding
|
||||
{
|
||||
Severity = required ? "Fehler" : "Warnung",
|
||||
Area = name,
|
||||
Message = "Datenerfassung fehlgeschlagen: " + exception.Message,
|
||||
RecommendedAction = "Logdatei, WMI-Provider und Leseberechtigungen prüfen."
|
||||
});
|
||||
logger.Error(name + ": " + exception.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Xml.Linq;
|
||||
using BizTalkApplicationCatalog.Models;
|
||||
using BizTalkApplicationCatalog.Reporting;
|
||||
|
||||
namespace BizTalkApplicationCatalog.Infrastructure
|
||||
{
|
||||
/// <summary>
|
||||
/// Prüft die zentrale Berichtslogik ohne Zugriff auf Windows- oder BizTalk-WMI.
|
||||
/// </summary>
|
||||
internal static class SelfTestRunner
|
||||
{
|
||||
public static void Run(string outputPath = null)
|
||||
{
|
||||
Assert(SensitiveDataSanitizer.Sanitize(
|
||||
"https://host/path?password=secret&client=100")
|
||||
== "https://host/path?password=[REDACTED]&client=100",
|
||||
"Secret-Redaktion ist fehlerhaft.");
|
||||
|
||||
var keepOutput = !string.IsNullOrWhiteSpace(outputPath);
|
||||
var directory = keepOutput
|
||||
? Path.GetDirectoryName(Path.GetFullPath(outputPath))
|
||||
: Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
"BizTalkApplicationCatalogTests-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(directory);
|
||||
var path = keepOutput ? Path.GetFullPath(outputPath) : Path.Combine(directory, "test.xlsx");
|
||||
try
|
||||
{
|
||||
var document = SampleDocument();
|
||||
new XlsxReportWriter().Write(document, path);
|
||||
Assert(File.Exists(path), "XLSX-Datei wurde nicht erzeugt.");
|
||||
|
||||
using (var archive = ZipFile.OpenRead(path))
|
||||
{
|
||||
var required = new[]
|
||||
{
|
||||
"[Content_Types].xml",
|
||||
"_rels/.rels",
|
||||
"docProps/core.xml",
|
||||
"docProps/app.xml",
|
||||
"xl/workbook.xml",
|
||||
"xl/_rels/workbook.xml.rels",
|
||||
"xl/styles.xml"
|
||||
};
|
||||
foreach (var name in required)
|
||||
{
|
||||
Assert(archive.GetEntry(name) != null, "XLSX-Part fehlt: " + name);
|
||||
}
|
||||
|
||||
for (var index = 1; index <= 10; index++)
|
||||
{
|
||||
Assert(
|
||||
archive.GetEntry("xl/worksheets/sheet" + index + ".xml") != null,
|
||||
"Arbeitsblatt fehlt: " + index);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
var workbook = LoadXml(archive, "xl/workbook.xml");
|
||||
XNamespace spreadsheet =
|
||||
"http://schemas.openxmlformats.org/spreadsheetml/2006/main";
|
||||
var names = workbook.Descendants(spreadsheet + "sheet")
|
||||
.Select(item => (string)item.Attribute("name")).ToList();
|
||||
Assert(names.Contains("Anwendungen"), "Anwendungsblatt fehlt.");
|
||||
Assert(names.Contains("Abdeckung"), "Abdeckungsblatt fehlt.");
|
||||
|
||||
var contentTypes = LoadXml(archive, "[Content_Types].xml");
|
||||
XNamespace contentTypeNamespace =
|
||||
"http://schemas.openxmlformats.org/package/2006/content-types";
|
||||
Assert(
|
||||
contentTypes.Root.Elements(contentTypeNamespace + "Override").Count() == 14,
|
||||
"Content-Type-Overrides sind unvollständig oder im falschen Namespace.");
|
||||
|
||||
var relationships = LoadXml(archive, "_rels/.rels");
|
||||
XNamespace relationshipNamespace =
|
||||
"http://schemas.openxmlformats.org/package/2006/relationships";
|
||||
Assert(
|
||||
relationships.Root.Elements(relationshipNamespace + "Relationship").Count() == 3,
|
||||
"Paketbeziehungen sind unvollständig oder im falschen Namespace.");
|
||||
|
||||
var combinedText = new StringBuilder();
|
||||
foreach (var entry in archive.Entries.Where(item =>
|
||||
item.FullName.StartsWith("xl/worksheets/", StringComparison.Ordinal)))
|
||||
{
|
||||
using (var reader = new StreamReader(entry.Open(), Encoding.UTF8))
|
||||
{
|
||||
combinedText.Append(reader.ReadToEnd());
|
||||
}
|
||||
}
|
||||
Assert(!combinedText.ToString().Contains("supersecret"), "XLSX enthält Testkennwort.");
|
||||
Assert(combinedText.ToString().Contains("[REDACTED]"), "Redaktionsmarker fehlt.");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!keepOutput && Directory.Exists(directory)) Directory.Delete(directory, true);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static InventoryDocument SampleDocument()
|
||||
{
|
||||
var document = new InventoryDocument
|
||||
{
|
||||
EnvironmentName = "ACC",
|
||||
ComputerName = "BIZTALK-ACC",
|
||||
ToolVersion = "1.0.0.0",
|
||||
ManagementServer = "SQL-ACC",
|
||||
ManagementDatabase = "BizTalkMgmtDb",
|
||||
StartedUtc = DateTime.UtcNow.AddSeconds(-1),
|
||||
CompletedUtc = DateTime.UtcNow
|
||||
};
|
||||
document.SystemProperties.Add(new NameValueRecord(
|
||||
"Betriebssystem", "Windows Server 2019", "Self-Test"));
|
||||
document.Applications.Add(new ApplicationRecord
|
||||
{
|
||||
Name = "OrderProcessing",
|
||||
Description = "Aufträge & Sonderzeichen <Test>",
|
||||
Status = "Gestartet (2)",
|
||||
IsDefault = "Nein",
|
||||
Source = "Self-Test"
|
||||
});
|
||||
var port = new ArtifactRecord
|
||||
{
|
||||
Type = "Send Port",
|
||||
ApplicationName = "OrderProcessing",
|
||||
Name = "Send_Orders",
|
||||
Status = "Gestartet (2)",
|
||||
HostName = "SendHost",
|
||||
AdapterName = "WCF-Custom",
|
||||
Address = SensitiveDataSanitizer.Sanitize(
|
||||
"https://service/orders?password=supersecret")
|
||||
};
|
||||
port.Properties.Add(new NameValueRecord("IsTwoWay", "True", "Self-Test"));
|
||||
document.Artifacts.Add(port);
|
||||
document.Coverage.Add(new CoverageRecord
|
||||
{
|
||||
DataSource = "MSBTS_Application",
|
||||
Status = "Vollständig",
|
||||
RowCount = 1,
|
||||
Required = "Ja",
|
||||
Message = "Self-Test"
|
||||
});
|
||||
document.SectionStatuses.Add(new SectionStatus
|
||||
{
|
||||
Name = "Self-Test",
|
||||
Status = "Erfolgreich",
|
||||
Message = "OK",
|
||||
Required = true,
|
||||
DurationMilliseconds = 1
|
||||
});
|
||||
return document;
|
||||
}
|
||||
|
||||
private static XDocument LoadXml(ZipArchive archive, string name)
|
||||
{
|
||||
using (var stream = archive.GetEntry(name).Open())
|
||||
{
|
||||
return XDocument.Load(stream);
|
||||
}
|
||||
}
|
||||
|
||||
private static void Assert(bool condition, string message)
|
||||
{
|
||||
if (!condition) throw new InvalidOperationException(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace BizTalkApplicationCatalog.Infrastructure
|
||||
{
|
||||
/// <summary>
|
||||
/// Verhindert, dass versehentlich Kennwörter oder Token aus WMI-Textwerten in den Bericht gelangen.
|
||||
/// </summary>
|
||||
internal static class SensitiveDataSanitizer
|
||||
{
|
||||
private static readonly Regex NamedSecret = new Regex(
|
||||
@"(?i)(password|passwd|pwd|secret|token|clientsecret|accesskey)\s*([=:])\s*([^;&\s""']+)",
|
||||
RegexOptions.Compiled | RegexOptions.CultureInvariant);
|
||||
|
||||
public static string Sanitize(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return NamedSecret.Replace(value, match =>
|
||||
match.Groups[1].Value + match.Groups[2].Value + "[REDACTED]");
|
||||
}
|
||||
|
||||
public static string RedactProperty(string name, string value)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(name)
|
||||
&& (name.IndexOf("password", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| name.IndexOf("secret", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| name.IndexOf("token", StringComparison.OrdinalIgnoreCase) >= 0))
|
||||
{
|
||||
return "[REDACTED]";
|
||||
}
|
||||
|
||||
return Sanitize(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user