namespace ZB.MOM.WW.MxGateway.Server.Diagnostics; /// /// Redacts sensitive information from log entries. /// public static class GatewayLogRedactor { /// Placeholder for redacted values. public const string RedactedValue = "[redacted]"; private static readonly HashSet SensitiveCommandMethods = new(StringComparer.OrdinalIgnoreCase) { "AuthenticateUser", // Carry a credential-sensitive write value (single item and bulk). "WriteSecured", "WriteSecured2", "WriteSecuredBulk", "WriteSecured2Bulk", // Carry operator_password. "AcknowledgeAlarm", "AcknowledgeAlarmByName" }; /// /// Authorization schemes whose name may survive redaction. Anything outside this list is /// dropped whole: an unrecognized leading word is as likely to be credential material as it /// is to be a scheme, so it is not worth the leak. /// private static readonly string[] KnownAuthorizationSchemes = [ "Bearer", "Basic", "Digest", "Negotiate", "NTLM", "ApiKey", "Token", ]; /// Prefix identifying a gateway-issued API key. private const string GatewayKeyPrefix = "mxgw_"; /// Upper bound on a key id kept in the clear; a longer run is treated as secret material. private const int MaxKeyIdLength = 64; /// /// Determines whether a command method bears credentials. /// /// The command method name to check. /// if the command method carries credentials; otherwise . public static bool IsCredentialBearingCommand(string? commandMethod) { return commandMethod is not null && SensitiveCommandMethods.Contains(commandMethod); } /// /// Redacts the credential portion of an authorization header value. /// /// The authorization header value to redact. /// The header with the credential redacted, or the original value when it is null or blank. public static string? RedactApiKey(string? authorizationHeader) { return RedactClientIdentity(authorizationHeader); } /// /// Redacts the credential carried by a client identity. Redaction fails closed: only a /// gateway-issued API key keeps its mxgw_<key-id>_ shape (so operators can tell keys /// apart in logs), and only a recognized scheme keeps its name. Every other value — a foreign /// bearer token, a scheme-less string, junk — is replaced whole, because nothing that reaches /// this method is known to be safe to log. /// /// The client identity string to redact. /// The redacted client identity, or the original value when it is null or blank. public static string? RedactClientIdentity(string? clientIdentity) { if (string.IsNullOrWhiteSpace(clientIdentity)) { return clientIdentity; } ReadOnlySpan value = clientIdentity.AsSpan().Trim(); int separatorIndex = value.IndexOf(' '); if (separatorIndex < 0) { // A single token carries no scheme, so the token itself is the credential. return RedactedValue; } ReadOnlySpan scheme = value[..separatorIndex]; ReadOnlySpan credential = value[(separatorIndex + 1)..].Trim(); if (credential.IsEmpty || !IsKnownAuthorizationScheme(scheme)) { return RedactedValue; } return credential.StartsWith(GatewayKeyPrefix, StringComparison.OrdinalIgnoreCase) ? $"{scheme} {GatewayKeyPrefix}{RedactKeyId(credential)}" : $"{scheme} {RedactedValue}"; } /// /// Renders the trailing portion of a gateway API key: the key id when the key is well formed, /// otherwise nothing but the placeholder. /// /// The credential, known to start with the gateway key prefix. /// The <key-id>_[redacted] tail, or just the placeholder. private static string RedactKeyId(ReadOnlySpan credential) { ReadOnlySpan remainder = credential[GatewayKeyPrefix.Length..]; int secretIndex = remainder.IndexOf('_'); // No separator means no secret boundary to trust, so the whole remainder is treated as secret. return secretIndex is <= 0 or > MaxKeyIdLength ? RedactedValue : $"{remainder[..secretIndex]}_{RedactedValue}"; } /// /// Determines whether a leading word is a recognized authorization scheme. /// /// The candidate scheme word. /// when the word may survive redaction; otherwise . private static bool IsKnownAuthorizationScheme(ReadOnlySpan scheme) { foreach (string knownScheme in KnownAuthorizationSchemes) { if (scheme.Equals(knownScheme, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } /// /// Redacts a command value if it contains credentials or value logging is disabled. /// /// The command method name to check for credentials. /// The command value to redact. /// Whether value logging is enabled. /// The redacted placeholder, the original value, or when is null. public static object? RedactCommandValue( string? commandMethod, object? value, bool valueLoggingEnabled = false) { if (value is null) { return null; } if (!valueLoggingEnabled || IsCredentialBearingCommand(commandMethod)) { return RedactedValue; } return value; } }