9cff87fe85
Remove project bookkeeping citations from shipped code comments across the solution: hyphenated task IDs (WP-14, StoreAndForward-025), milestone/task/ issue refs (M3, Task 4, Audit Log #23, #21), Bundle X task-bundle labels, and C/D/K/S/T phase labels. Comment text only — no code logic, string/log literals, or XML-doc structure changed. Genuine descriptions are preserved (only the citation is stripped), and technical lookalikes are retained (UTF-8, SHA-256, T00:00:00, M365, UTC-5, pre-C4/pre-C5 schema versions). Flagged by the new CommentChecker TaskReferenceInComment / TrackingReferenceInComment checks plus targeted grep passes; full solution builds clean, append-only guard tests pass.
92 lines
3.8 KiB
C#
92 lines
3.8 KiB
C#
namespace ZB.MOM.WW.ScadaBridge.NotificationService;
|
|
|
|
/// <summary>
|
|
/// Scrubs SMTP credential secrets out of free text (typically exception
|
|
/// messages echoed back by an SMTP server) before that text is written to a log.
|
|
/// MailKit authentication exceptions can contain server responses that quote the
|
|
/// supplied credentials; this prevents a password, client secret, or OAuth2 token
|
|
/// from leaking into the operational logs.
|
|
/// <para>
|
|
/// Public so the central Notification Outbox's <c>EmailNotificationDeliveryAdapter</c>
|
|
/// can share this exact redaction logic rather than carry a divergent copy.
|
|
/// </para>
|
|
/// </summary>
|
|
public static class CredentialRedactor
|
|
{
|
|
private const string Mask = "***REDACTED***";
|
|
|
|
/// <summary>
|
|
/// Returns <paramref name="text"/> with every secret component of the supplied
|
|
/// colon-delimited credential string masked.
|
|
/// </summary>
|
|
/// <param name="text">The text to scrub (e.g. an exception message).</param>
|
|
/// <param name="credentials">
|
|
/// The credential string in use — Basic Auth <c>user:pass</c> or OAuth2
|
|
/// <c>tenantId:clientId:clientSecret</c>. May be null.
|
|
/// </param>
|
|
/// <summary>
|
|
/// Minimum length for a colon-separated SECRET component to be
|
|
/// considered worth masking. Twelve characters is the standard heuristic
|
|
/// for "long enough to be a password / client secret"; shorter components
|
|
/// (e.g. a 4-char user name like <c>root</c>, or a 7-char "from" alias)
|
|
/// would mask too much unrelated diagnostic text if treated as secrets.
|
|
/// </summary>
|
|
private const int MinSecretLength = 12;
|
|
|
|
/// <summary>
|
|
/// Returns <paramref name="text"/> with every secret component of the supplied
|
|
/// colon-delimited credential string masked.
|
|
/// </summary>
|
|
/// <param name="text">The text to scrub (e.g. an exception message). Returns empty string if null.</param>
|
|
/// <param name="credentials">
|
|
/// The credential string in use — Basic Auth <c>user:pass</c> or OAuth2
|
|
/// <c>tenantId:clientId:clientSecret</c>. May be null; returns <paramref name="text"/> unmodified when null.
|
|
/// </param>
|
|
/// <returns>The scrubbed text with secret components replaced by <c>***REDACTED***</c>.</returns>
|
|
public static string Scrub(string? text, string? credentials)
|
|
{
|
|
if (string.IsNullOrEmpty(text) || string.IsNullOrEmpty(credentials))
|
|
{
|
|
return text ?? string.Empty;
|
|
}
|
|
|
|
var result = text;
|
|
|
|
// Redact only the obviously-secret slots — the LAST
|
|
// colon-separated component (the password in Basic, the client
|
|
// secret in OAuth2) and the whole packed string — not the user
|
|
// name / tenant id / client id. A short user name like "root" or
|
|
// a sender alias like "smtp" no longer becomes a global redaction
|
|
// token that eats unrelated path / error text.
|
|
var secretsToRedact = new List<string>();
|
|
|
|
// The full packed credential is always the most-sensitive shape.
|
|
secretsToRedact.Add(credentials);
|
|
|
|
// The trailing colon-component is the password / clientSecret slot.
|
|
// Only redact it if it's plausibly secret-shaped (>= MinSecretLength).
|
|
var parts = credentials.Split(':');
|
|
if (parts.Length >= 2)
|
|
{
|
|
var lastComponent = parts[^1];
|
|
if (lastComponent.Length >= MinSecretLength)
|
|
{
|
|
secretsToRedact.Add(lastComponent);
|
|
}
|
|
}
|
|
|
|
// Order longest first so a secret that is a substring of the packed
|
|
// string is still fully masked.
|
|
var ordered = secretsToRedact
|
|
.Distinct()
|
|
.OrderByDescending(s => s.Length);
|
|
|
|
foreach (var part in ordered)
|
|
{
|
|
result = result.Replace(part, Mask, StringComparison.Ordinal);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|