Harden endpoint reachability resolution

This commit is contained in:
2026-08-06 15:58:18 +02:00
parent 82584473e8
commit ac51714a88
19 changed files with 556 additions and 152 deletions
+151 -54
View File
@@ -21,33 +21,77 @@ namespace BizTalkCheckmkPulse
out EndpointCatalogEntry entry,
out string reason)
{
entry = null;
reason = string.Empty;
var resolution = Analyze(candidate);
entry = resolution.Entry;
reason = resolution.Reason ?? string.Empty;
return resolution.Status == EndpointResolutionStatus.Probeable;
}
/// <summary>
/// Trennt sicher pruefbare Ziele von erwartbar lokalen/adapterinternen Adressen und echten Aufloesungsluecken.
/// </summary>
public static EndpointResolution Analyze(EndpointCandidate candidate)
{
if (candidate == null)
{
reason = "Endpoint-Kandidat fehlt.";
return false;
return Unresolved("Endpoint-Kandidat fehlt.");
}
if (candidate.Dynamic)
{
reason = "Dynamischer Send Port besitzt kein statisch pruefbares Ziel.";
return false;
return Expected("Dynamischer Send Port besitzt kein statisch pruefbares Ziel.");
}
var address = (candidate.Address ?? string.Empty).Trim();
if (address.Length == 0)
{
reason = "Transportadresse ist leer.";
return false;
return Unresolved("Aktive statische Transportadresse ist leer.");
}
if (LooksLikeEmailRecipients(address) || Contains(candidate.AdapterName, "SMTP"))
{
return Expected("SMTP-Empfaenger beziehungsweise Adapteradresse enthaelt nicht den SMTP-Server.");
}
if (Regex.IsMatch(address, @"^[a-zA-Z]:[\\/]", RegexOptions.CultureInvariant)
|| Contains(candidate.AdapterName, "FILE") && !address.StartsWith("\\\\", StringComparison.Ordinal))
{
return Expected("Lokaler Dateipfad benoetigt keine Netzwerkprobe.");
}
if (address.StartsWith("net.pipe:", StringComparison.OrdinalIgnoreCase)
|| address.StartsWith("npipe:", StringComparison.OrdinalIgnoreCase))
{
return Expected("Named Pipes besitzen kein TCP-/UDP-Ziel.");
}
if (address.StartsWith("/", StringComparison.Ordinal)
&& IsInboundListener(candidate))
{
return Expected("Relative Receive-Listener-Adresse enthaelt keine eindeutige Site-Bindung.");
}
EndpointCatalogEntry entry;
string protocol;
string host;
int port;
if (!TryResolve(address, candidate.AdapterName, out protocol, out host, out port, out reason))
string reason;
if (!TryResolve(
address,
candidate.AdapterName,
IsInboundListener(candidate),
out protocol,
out host,
out port,
out reason))
{
return false;
if (address.StartsWith("file:", StringComparison.OrdinalIgnoreCase)
|| address.StartsWith("mailto:", StringComparison.OrdinalIgnoreCase))
{
return Expected(reason);
}
return Unresolved(reason);
}
entry = new EndpointCatalogEntry
@@ -64,7 +108,12 @@ namespace BizTalkCheckmkPulse
Enabled = true,
AutoDiscovered = true
};
return true;
return new EndpointResolution
{
Status = EndpointResolutionStatus.Probeable,
Entry = entry,
Reason = string.Empty
};
}
/// <summary>
@@ -72,52 +121,13 @@ namespace BizTalkCheckmkPulse
/// </summary>
public static bool IsPotentialExternalEndpoint(EndpointCandidate candidate)
{
if (candidate == null || candidate.Dynamic)
{
return false;
}
var address = (candidate.Address ?? string.Empty).Trim();
if (address.Length == 0
|| LooksLikeEmailRecipients(address)
|| Regex.IsMatch(address, @"^[a-zA-Z]:[\\/]", RegexOptions.CultureInvariant)
|| address.StartsWith("/", StringComparison.Ordinal)
|| address.StartsWith("net.pipe:", StringComparison.OrdinalIgnoreCase)
|| address.StartsWith("npipe:", StringComparison.OrdinalIgnoreCase))
{
return false;
}
if (Contains(candidate.AdapterName, "SMTP")
|| Contains(candidate.AdapterName, "FILE") && !address.StartsWith("\\\\", StringComparison.Ordinal))
{
return false;
}
if (address.StartsWith("\\\\", StringComparison.Ordinal)
|| HostPortPattern.IsMatch(address)
|| Regex.IsMatch(address, @"(?:FORMATNAME:)?DIRECT=(?:OS|TCP):", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant))
{
return true;
}
Uri uri;
if (Uri.TryCreate(address, UriKind.Absolute, out uri))
{
return !string.Equals(uri.Scheme, "file", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(uri.Scheme, "mailto", StringComparison.OrdinalIgnoreCase);
}
return Contains(candidate.AdapterName, "SFTP")
|| Contains(candidate.AdapterName, "FTP")
|| Contains(candidate.AdapterName, "HTTP")
|| Contains(candidate.AdapterName, "SOAP")
|| Contains(candidate.AdapterName, "WCF");
return Analyze(candidate).Status == EndpointResolutionStatus.Unresolved;
}
private static bool TryResolve(
string address,
string adapterName,
bool allowWildcardListener,
out string protocol,
out string host,
out int port,
@@ -128,6 +138,12 @@ namespace BizTalkCheckmkPulse
port = 0;
reason = string.Empty;
if (allowWildcardListener
&& TryResolveWildcardListener(address, out protocol, out host, out port, out reason))
{
return true;
}
if (LooksLikeEmailRecipients(address))
{
reason = "SMTP-Empfaengerliste ist kein pruefbarer Netzwerk-Endpunkt.";
@@ -210,14 +226,14 @@ namespace BizTalkCheckmkPulse
if (Contains(adapterName, "SFTP"))
{
host = address.Trim('/');
host = ExtractBareAdapterHost(address);
port = 22;
return Validate(host, port, out reason);
}
if (Contains(adapterName, "FTP"))
{
host = address.Trim('/');
host = ExtractBareAdapterHost(address);
port = 21;
return Validate(host, port, out reason);
}
@@ -248,10 +264,91 @@ namespace BizTalkCheckmkPulse
case "imap": return 143;
case "imaps": return 993;
case "msmq": return 1801;
case "net.tcp": return 808;
case "mssql": return 1433;
// Ohne Pfad ist der Hostteil ein tnsnames.ora-Alias und kein sicher pruefbarer DNS-Host.
case "oracledb": return uri.AbsolutePath.Trim('/').Length > 0 ? 1521 : 0;
default: return uri.Port > 0 ? uri.Port : 0;
}
}
/// <summary>
/// Reduziert lokale HTTP-/WCF-Wildcard-Listener auf einen nebenwirkungsfreien Loopback-Porttest.
/// </summary>
private static bool TryResolveWildcardListener(
string address,
out string protocol,
out string host,
out int port,
out string reason)
{
protocol = "TCP";
host = string.Empty;
port = 0;
reason = string.Empty;
var match = Regex.Match(
address,
@"^(?<scheme>https?|net\.tcp)://(?:\+|\*)(?::(?<port>\d{1,5}))?(?:[/\\].*)?$",
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
if (!match.Success)
{
return false;
}
var scheme = match.Groups["scheme"].Value.ToLowerInvariant();
if (!string.IsNullOrWhiteSpace(match.Groups["port"].Value)
&& !int.TryParse(match.Groups["port"].Value, out port))
{
port = 0;
}
if (port <= 0)
{
port = scheme == "https" ? 443 : scheme == "net.tcp" ? 808 : 80;
}
host = "127.0.0.1";
return Validate(host, port, out reason);
}
private static string ExtractBareAdapterHost(string address)
{
var value = (address ?? string.Empty).Trim().Trim('/');
var separator = value.IndexOfAny(new[] { '/', '\\' });
return separator < 0 ? value : value.Substring(0, separator);
}
/// <summary>
/// Erkennt Adapter, deren relative Receive-Adresse eine lokale Listenerbindung statt eines Remoteziels beschreibt.
/// </summary>
private static bool IsInboundListener(EndpointCandidate candidate)
{
return string.Equals(candidate.ArtifactType, "ReceiveLocation", StringComparison.OrdinalIgnoreCase)
&& (Contains(candidate.AdapterName, "HTTP")
|| Contains(candidate.AdapterName, "SOAP")
|| Contains(candidate.AdapterName, "WCF"));
}
private static EndpointResolution Expected(string reason)
{
return new EndpointResolution
{
Status = EndpointResolutionStatus.ExpectedNonProbeable,
Reason = reason
};
}
private static EndpointResolution Unresolved(string reason)
{
return new EndpointResolution
{
Status = EndpointResolutionStatus.Unresolved,
Reason = string.IsNullOrWhiteSpace(reason)
? "Transportadresse konnte nicht sicher auf Host und Port reduziert werden."
: reason
};
}
private static bool HasExplicitPort(string address)
{
var authorityEnd = address.IndexOfAny(new[] { '/', '?' }, address.IndexOf("://", StringComparison.Ordinal) + 3);