using System.Text.RegularExpressions; using ZB.MOM.WW.Audit; using ZB.MOM.WW.ScadaBridge.AuditLog.Payload; using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit; using static ZB.MOM.WW.ScadaBridge.AuditLog.Payload.AuditRedactionPrimitives; namespace ZB.MOM.WW.ScadaBridge.AuditLog.Redaction; /// /// Minimal always-safe fallback for composition /// roots that bypass the full . /// Performs line-oriented HTTP header /// redaction for the always-sensitive defaults (Authorization, X-Api-Key, /// Cookie, Set-Cookie) on the RequestSummary / ResponseSummary /// fields carried inside ZB.MOM.WW.Audit.AuditEvent.DetailsJson. Does NOT /// perform body-regex redaction, SQL-parameter redaction, or truncation — those /// need with live options. Contract: /// over-redact safely, never throw, never miss a header on the default /// sensitive list. /// public sealed class SafeDefaultAuditRedactor : IAuditRedactor { /// Singleton instance — the redactor is stateless and side-effect-free. public static SafeDefaultAuditRedactor Instance { get; } = new SafeDefaultAuditRedactor(); private static readonly string[] DefaultHeaderRedactList = { "Authorization", "X-Api-Key", "Cookie", "Set-Cookie", }; private static readonly Regex HeaderRegex = new( @"(?[A-Za-z][A-Za-z0-9\-_]*)\s*:\s*(?[^\r\n]*)", RegexOptions.Compiled | RegexOptions.IgnoreCase); private SafeDefaultAuditRedactor() { } /// /// Applies line-oriented header redaction to the default sensitive headers /// (Authorization, X-Api-Key, Cookie, Set-Cookie) /// found in RequestSummary and ResponseSummary inside /// .DetailsJson. Never throws; over-redacts on /// any internal failure. /// /// The audit event whose details JSON is to be redacted. /// A new with sensitive headers replaced by the redacted marker, or an over-redacted sentinel on failure. public AuditEvent Apply(AuditEvent rawEvent) { ArgumentNullException.ThrowIfNull(rawEvent); // Fast path: no DetailsJson means no summaries to scrub. if (string.IsNullOrEmpty(rawEvent.DetailsJson)) { return rawEvent; } try { var d = AuditDetailsCodec.Deserialize(rawEvent.DetailsJson); var scrubbed = d with { RequestSummary = RedactHeaders(d.RequestSummary), ResponseSummary = RedactHeaders(d.ResponseSummary), }; return rawEvent with { DetailsJson = AuditDetailsCodec.Serialize(scrubbed) }; } catch { // Over-redact: suppress ALL sensitive free-text fields so a failure // on any internal path never leaks the original. The contract is // "never throw." Uses the shared OverRedactedEventMarker so all // redactor safety-nets emit the same sentinel string. var safe = new AuditDetails { RequestSummary = OverRedactedEventMarker, ResponseSummary = OverRedactedEventMarker, ErrorDetail = OverRedactedEventMarker, ErrorMessage = OverRedactedEventMarker, Extra = OverRedactedEventMarker, PayloadTruncated = true, }; return rawEvent with { DetailsJson = AuditDetailsCodec.Serialize(safe) }; } } private static string? RedactHeaders(string? summary) { if (string.IsNullOrEmpty(summary)) return summary; return HeaderRegex.Replace(summary, m => { var name = m.Groups["name"].Value; foreach (var sensitive in DefaultHeaderRedactList) { if (string.Equals(name, sensitive, StringComparison.OrdinalIgnoreCase)) { // Use the shared RedactedMarker so line-format and JSON-format // header redaction emit the same sentinel string. return $"{name}: {RedactedMarker}"; } } return m.Value; }); } }