Improve certificate path probing with demo UNC target and remove Sonic container scan.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,302 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using ZA.CoreService.ESBCertificateManager.Models;
|
||||
|
||||
namespace ZA.CoreService.ESBCertificateManager.Services;
|
||||
|
||||
public sealed class CertificateProbeService
|
||||
{
|
||||
public static readonly string[] SupportedExtensions =
|
||||
[
|
||||
".cer",
|
||||
".crt",
|
||||
".pem",
|
||||
".pfx",
|
||||
".p12"
|
||||
];
|
||||
|
||||
public CertificateProbeResult Probe(
|
||||
string certificatePath,
|
||||
Func<string?>? passwordPrompt = null)
|
||||
{
|
||||
string? normalized = NormalizePath(certificatePath);
|
||||
|
||||
if (normalized is null)
|
||||
{
|
||||
return CertificateProbeResult.Failed(
|
||||
"Kein gültiger Zertifikatspfad angegeben.");
|
||||
}
|
||||
|
||||
if (!IsSupportedExtension(normalized))
|
||||
{
|
||||
return CertificateProbeResult.Failed(
|
||||
"Nicht unterstütztes Format. Erlaubt: "
|
||||
+ string.Join(", ", SupportedExtensions));
|
||||
}
|
||||
|
||||
if (!File.Exists(normalized))
|
||||
{
|
||||
return CertificateProbeResult.Missing(
|
||||
normalized,
|
||||
"Am Zielpfad liegt noch keine Zertifikatsdatei.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
CertificateInfo info = ReadCertificate(normalized, password: null);
|
||||
return CertificateProbeResult.Found(normalized, info);
|
||||
}
|
||||
catch (CryptographicException) when (IsPasswordProtected(normalized))
|
||||
{
|
||||
if (passwordPrompt is null)
|
||||
{
|
||||
return CertificateProbeResult.PasswordRequired(normalized);
|
||||
}
|
||||
|
||||
string? password = passwordPrompt.Invoke();
|
||||
|
||||
if (password is null)
|
||||
{
|
||||
return CertificateProbeResult.Failed(
|
||||
"Zum Lesen der PFX/P12-Datei wird ein Kennwort benötigt.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
CertificateInfo info = ReadCertificate(normalized, password);
|
||||
return CertificateProbeResult.Found(normalized, info);
|
||||
}
|
||||
catch (CryptographicException)
|
||||
{
|
||||
return CertificateProbeResult.Failed(
|
||||
"Das Kennwort ist falsch oder die Datei ist beschädigt.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return CertificateProbeResult.Failed(
|
||||
$"Zertifikat konnte nicht gelesen werden: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TrySplitTargetPath(
|
||||
string fullPath,
|
||||
out string directory,
|
||||
out string fileName,
|
||||
out string? error)
|
||||
{
|
||||
directory = string.Empty;
|
||||
fileName = string.Empty;
|
||||
error = null;
|
||||
|
||||
string? normalized = NormalizePath(fullPath);
|
||||
|
||||
if (normalized is null)
|
||||
{
|
||||
error = "Bitte einen vollständigen Pfad zur Zertifikatsdatei angeben.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IsSupportedExtension(normalized))
|
||||
{
|
||||
error =
|
||||
"Dateiendung nicht unterstützt. Erlaubt: "
|
||||
+ string.Join(", ", SupportedExtensions);
|
||||
return false;
|
||||
}
|
||||
|
||||
string? dir = Path.GetDirectoryName(normalized);
|
||||
string file = Path.GetFileName(normalized);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(dir) || string.IsNullOrWhiteSpace(file))
|
||||
{
|
||||
error =
|
||||
"Pfad muss Verzeichnis und Dateiname enthalten, z. B.\n"
|
||||
+ @"\\server\e$\...\certs\server.p12";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Path.GetFileName(file) != file)
|
||||
{
|
||||
error = "Dateiname darf keinen weiteren Verzeichnisanteil enthalten.";
|
||||
return false;
|
||||
}
|
||||
|
||||
directory = dir;
|
||||
fileName = file;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static string CombineTargetPath(
|
||||
string directory,
|
||||
string fileName)
|
||||
{
|
||||
return Path.Combine(
|
||||
directory.Trim().TrimEnd('\\', '/'),
|
||||
fileName.Trim());
|
||||
}
|
||||
|
||||
public static bool IsSupportedExtension(string path)
|
||||
{
|
||||
string extension = Path.GetExtension(path);
|
||||
return SupportedExtensions.Contains(
|
||||
extension,
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool IsPasswordProtected(string path)
|
||||
{
|
||||
string extension = Path.GetExtension(path);
|
||||
return extension.Equals(".pfx", StringComparison.OrdinalIgnoreCase)
|
||||
|| extension.Equals(".p12", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string? NormalizePath(string? path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string trimmed = path.Trim().Trim('"');
|
||||
|
||||
if (trimmed.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// UNC und absolute lokale Pfade akzeptieren
|
||||
if (!Path.IsPathRooted(trimmed)
|
||||
&& !trimmed.StartsWith(@"\\", StringComparison.Ordinal))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
private static CertificateInfo ReadCertificate(
|
||||
string certificatePath,
|
||||
string? password)
|
||||
{
|
||||
string extension = Path.GetExtension(certificatePath);
|
||||
|
||||
using X509Certificate2 certificate =
|
||||
IsPasswordProtected(certificatePath)
|
||||
? new X509Certificate2(
|
||||
certificatePath,
|
||||
password,
|
||||
X509KeyStorageFlags.EphemeralKeySet)
|
||||
: new X509Certificate2(certificatePath);
|
||||
|
||||
string subject = certificate.GetNameInfo(
|
||||
X509NameType.SimpleName,
|
||||
forIssuer: false);
|
||||
|
||||
string issuer = certificate.GetNameInfo(
|
||||
X509NameType.SimpleName,
|
||||
forIssuer: true);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(subject))
|
||||
{
|
||||
subject = certificate.Subject;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(issuer))
|
||||
{
|
||||
issuer = certificate.Issuer;
|
||||
}
|
||||
|
||||
string fingerprint = certificate.GetCertHashString(
|
||||
HashAlgorithmName.SHA256);
|
||||
|
||||
fingerprint = string.Join(
|
||||
":",
|
||||
Enumerable.Range(0, fingerprint.Length / 2)
|
||||
.Select(index => fingerprint.Substring(index * 2, 2)));
|
||||
|
||||
DateTimeOffset validFrom = new(certificate.NotBefore);
|
||||
DateTimeOffset validUntil = new(certificate.NotAfter);
|
||||
DateTimeOffset now = DateTimeOffset.Now;
|
||||
|
||||
return new CertificateInfo
|
||||
{
|
||||
Subject = subject,
|
||||
Issuer = issuer,
|
||||
FingerprintSha256 = fingerprint,
|
||||
ValidFrom = validFrom,
|
||||
ValidUntil = validUntil,
|
||||
IsCurrentlyValid = now >= validFrom && now <= validUntil
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class CertificateProbeResult
|
||||
{
|
||||
public bool Success { get; init; }
|
||||
|
||||
public bool FileExists { get; init; }
|
||||
|
||||
public string Path { get; init; } = string.Empty;
|
||||
|
||||
public string Message { get; init; } = string.Empty;
|
||||
|
||||
public CertificateInfo? Certificate { get; init; }
|
||||
|
||||
public static CertificateProbeResult Found(
|
||||
string path,
|
||||
CertificateInfo certificate)
|
||||
{
|
||||
string expiry = certificate.ValidUntil.ToLocalTime()
|
||||
.ToString("dd.MM.yyyy HH:mm");
|
||||
|
||||
string validity = certificate.IsCurrentlyValid
|
||||
? "gültig"
|
||||
: "abgelaufen / ungültig";
|
||||
|
||||
return new CertificateProbeResult
|
||||
{
|
||||
Success = true,
|
||||
FileExists = true,
|
||||
Path = path,
|
||||
Certificate = certificate,
|
||||
Message =
|
||||
$"Zertifikat gefunden · Subject: {certificate.Subject} · "
|
||||
+ $"Ablauf: {expiry} ({validity})"
|
||||
};
|
||||
}
|
||||
|
||||
public static CertificateProbeResult Missing(
|
||||
string path,
|
||||
string message)
|
||||
{
|
||||
return new CertificateProbeResult
|
||||
{
|
||||
Success = true,
|
||||
FileExists = false,
|
||||
Path = path,
|
||||
Message = message
|
||||
};
|
||||
}
|
||||
|
||||
public static CertificateProbeResult Failed(string message)
|
||||
{
|
||||
return new CertificateProbeResult
|
||||
{
|
||||
Success = false,
|
||||
FileExists = false,
|
||||
Message = message
|
||||
};
|
||||
}
|
||||
|
||||
public static CertificateProbeResult PasswordRequired(string path)
|
||||
{
|
||||
return new CertificateProbeResult
|
||||
{
|
||||
Success = false,
|
||||
FileExists = true,
|
||||
Path = path,
|
||||
Message = "PFX/P12 vorhanden – Kennwort nötig zum Lesen des Ablaufs"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,8 @@ public sealed class LocalCertificateDeployer
|
||||
".cer",
|
||||
".crt",
|
||||
".pem",
|
||||
".pfx"
|
||||
".pfx",
|
||||
".p12"
|
||||
};
|
||||
|
||||
public async Task<string> DeployAsync(
|
||||
|
||||
@@ -1,303 +0,0 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using ZA.CoreService.ESBCertificateManager.Models;
|
||||
|
||||
namespace ZA.CoreService.ESBCertificateManager.Services;
|
||||
|
||||
public sealed class SonicContainerScanner
|
||||
{
|
||||
private readonly RuntimeSettings _runtimeSettings;
|
||||
|
||||
public SonicContainerScanner(RuntimeSettings runtimeSettings)
|
||||
{
|
||||
_runtimeSettings = runtimeSettings
|
||||
?? throw new ArgumentNullException(nameof(runtimeSettings));
|
||||
}
|
||||
|
||||
public async Task<ContainerScanResult> ScanAsync(
|
||||
SonicSystemOption system,
|
||||
string userName,
|
||||
string secret,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(system);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(userName))
|
||||
{
|
||||
return ContainerScanResult.Failed(
|
||||
"Der Sonic-Benutzername fehlt.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(secret))
|
||||
{
|
||||
return ContainerScanResult.Failed(
|
||||
"Das Sonic-Kennwort fehlt.");
|
||||
}
|
||||
|
||||
string javaPath =
|
||||
PathResolver.ResolvePath(
|
||||
_runtimeSettings.JavaExecutablePath);
|
||||
|
||||
string libraryPath =
|
||||
PathResolver.ResolvePath(
|
||||
_runtimeSettings.SonicClientLibraryPath);
|
||||
|
||||
string? toolsPath = ResolveToolsDirectory();
|
||||
|
||||
if (!File.Exists(javaPath))
|
||||
{
|
||||
return ContainerScanResult.Failed(
|
||||
$"Java wurde nicht gefunden: {javaPath}");
|
||||
}
|
||||
|
||||
if (!Directory.Exists(libraryPath))
|
||||
{
|
||||
return ContainerScanResult.Failed(
|
||||
$"Der Sonic-JAR-Ordner wurde nicht gefunden: {libraryPath}");
|
||||
}
|
||||
|
||||
if (toolsPath is null)
|
||||
{
|
||||
return ContainerScanResult.Failed(
|
||||
"SonicMfContainerTool.class wurde nicht gefunden.");
|
||||
}
|
||||
|
||||
string[] jars =
|
||||
Directory.GetFiles(
|
||||
libraryPath,
|
||||
"*.jar",
|
||||
SearchOption.TopDirectoryOnly);
|
||||
|
||||
if (jars.Length == 0)
|
||||
{
|
||||
return ContainerScanResult.Failed(
|
||||
"Im Sonic-Client-Ordner wurden keine JAR-Dateien gefunden.");
|
||||
}
|
||||
|
||||
string classpath =
|
||||
toolsPath
|
||||
+ Path.PathSeparator
|
||||
+ string.Join(
|
||||
Path.PathSeparator,
|
||||
jars.OrderBy(
|
||||
Path.GetFileName,
|
||||
StringComparer.OrdinalIgnoreCase));
|
||||
|
||||
ProcessStartInfo startInfo = new()
|
||||
{
|
||||
FileName = javaPath,
|
||||
WorkingDirectory = toolsPath,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
StandardOutputEncoding = Encoding.UTF8,
|
||||
StandardErrorEncoding = Encoding.UTF8,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
startInfo.ArgumentList.Add("-cp");
|
||||
startInfo.ArgumentList.Add(classpath);
|
||||
startInfo.ArgumentList.Add("SonicMfContainerTool");
|
||||
startInfo.ArgumentList.Add("list");
|
||||
startInfo.ArgumentList.Add("--domain");
|
||||
startInfo.ArgumentList.Add(system.DomainName);
|
||||
startInfo.ArgumentList.Add("--url");
|
||||
startInfo.ArgumentList.Add(system.ConnectionUrl);
|
||||
startInfo.ArgumentList.Add("--user");
|
||||
startInfo.ArgumentList.Add(userName.Trim());
|
||||
startInfo.ArgumentList.Add("--timeout");
|
||||
startInfo.ArgumentList.Add("45");
|
||||
|
||||
startInfo.Environment["ESB_SONIC_PASSWORD"] = secret;
|
||||
|
||||
using Process process = new()
|
||||
{
|
||||
StartInfo = startInfo
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
if (!process.Start())
|
||||
{
|
||||
return ContainerScanResult.Failed(
|
||||
"Der Java-Prozess konnte nicht gestartet werden.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ContainerScanResult.Failed(
|
||||
$"Java konnte nicht gestartet werden: {ex.Message}");
|
||||
}
|
||||
|
||||
Task<string> standardOutputTask =
|
||||
process.StandardOutput.ReadToEndAsync();
|
||||
|
||||
Task<string> standardErrorTask =
|
||||
process.StandardError.ReadToEndAsync();
|
||||
|
||||
using CancellationTokenSource timeoutSource =
|
||||
CancellationTokenSource.CreateLinkedTokenSource(
|
||||
cancellationToken);
|
||||
|
||||
timeoutSource.CancelAfter(TimeSpan.FromSeconds(50));
|
||||
|
||||
try
|
||||
{
|
||||
await process.WaitForExitAsync(timeoutSource.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
when (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
TryKill(process);
|
||||
|
||||
return ContainerScanResult.Failed(
|
||||
"Der Container-Scan hat das Zeitlimit überschritten.");
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
TryKill(process);
|
||||
throw;
|
||||
}
|
||||
|
||||
string standardOutput =
|
||||
(await standardOutputTask).Trim();
|
||||
|
||||
string standardError =
|
||||
(await standardErrorTask).Trim();
|
||||
|
||||
string combined =
|
||||
string.Join(
|
||||
Environment.NewLine,
|
||||
new[] { standardOutput, standardError }
|
||||
.Where(value => !string.IsNullOrWhiteSpace(value)));
|
||||
|
||||
bool success =
|
||||
process.ExitCode == 0
|
||||
&& combined.Contains(
|
||||
"OK:ContainerListEnd",
|
||||
StringComparison.OrdinalIgnoreCase)
|
||||
&& !combined.Contains(
|
||||
"ERROR:",
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
return ContainerScanResult.Failed(
|
||||
ExtractError(combined)
|
||||
?? $"Container-Scan fehlgeschlagen, ExitCode={process.ExitCode}.");
|
||||
}
|
||||
|
||||
List<string> containers = [];
|
||||
|
||||
foreach (string line in standardOutput.Split(
|
||||
['\r', '\n'],
|
||||
StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
const string prefix = "CONTAINER=";
|
||||
|
||||
if (!line.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string name = line[prefix.Length..].Trim();
|
||||
|
||||
if (name.Length > 0
|
||||
&& !containers.Contains(name, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
containers.Add(name);
|
||||
}
|
||||
}
|
||||
|
||||
containers.Sort(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
return ContainerScanResult.Successful(containers);
|
||||
}
|
||||
|
||||
private static string? ResolveToolsDirectory()
|
||||
{
|
||||
string baseDirectory = AppContext.BaseDirectory;
|
||||
|
||||
string[] candidates =
|
||||
[
|
||||
Path.Combine(baseDirectory, "Tools"),
|
||||
Path.Combine(baseDirectory, "..", "..", "..", "Tools"),
|
||||
Path.Combine(
|
||||
Directory.GetCurrentDirectory(),
|
||||
"Tools")
|
||||
];
|
||||
|
||||
foreach (string candidate in candidates)
|
||||
{
|
||||
string fullPath = Path.GetFullPath(candidate);
|
||||
string classFile = Path.Combine(
|
||||
fullPath,
|
||||
"SonicMfContainerTool.class");
|
||||
|
||||
if (File.Exists(classFile))
|
||||
{
|
||||
return fullPath;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string? ExtractError(string output)
|
||||
{
|
||||
foreach (string line in output.Split(
|
||||
['\r', '\n'],
|
||||
StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
if (line.StartsWith("ERROR:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return line["ERROR:".Length..].Trim();
|
||||
}
|
||||
}
|
||||
|
||||
return string.IsNullOrWhiteSpace(output) ? null : output;
|
||||
}
|
||||
|
||||
private static void TryKill(Process process)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!process.HasExited)
|
||||
{
|
||||
process.Kill(entireProcessTree: true);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ContainerScanResult
|
||||
{
|
||||
public bool Success { get; init; }
|
||||
|
||||
public string Message { get; init; } = string.Empty;
|
||||
|
||||
public IReadOnlyList<string> Containers { get; init; } = [];
|
||||
|
||||
public static ContainerScanResult Successful(
|
||||
IReadOnlyList<string> containers)
|
||||
{
|
||||
return new ContainerScanResult
|
||||
{
|
||||
Success = true,
|
||||
Message = $"{containers.Count} Container gefunden.",
|
||||
Containers = containers
|
||||
};
|
||||
}
|
||||
|
||||
public static ContainerScanResult Failed(string message)
|
||||
{
|
||||
return new ContainerScanResult
|
||||
{
|
||||
Success = false,
|
||||
Message = message
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user