Files
ScadaBridge/src/ZB.MOM.WW.ScadaBridge.AuditLog/Redaction/SafeDefaultAuditRedactor.cs
T
Joseph Doherty eabf270d71 docs: complete XML doc coverage (returns, summaries, inheritdoc)
Resolve all 622 issues flagged by the enhanced CommentChecker: add missing
<returns> tags (incl. the standard phrasing on non-generic Task methods),
add missing <summary> tags, and replace misused/redundant <inheritdoc/> on
members that override or implement nothing with real documentation.
Documentation-only — no behavior change; solution builds clean.
2026-06-03 11:39:32 -04:00

108 lines
4.4 KiB
C#

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;
/// <summary>
/// Minimal always-safe <see cref="IAuditRedactor"/> fallback for composition
/// roots that bypass the full <see cref="ScadaBridgeAuditRedactor"/>.
/// Performs line-oriented HTTP header
/// redaction for the always-sensitive defaults (Authorization, X-Api-Key,
/// Cookie, Set-Cookie) on the <c>RequestSummary</c> / <c>ResponseSummary</c>
/// fields carried inside <c>ZB.MOM.WW.Audit.AuditEvent.DetailsJson</c>. Does NOT
/// perform body-regex redaction, SQL-parameter redaction, or truncation — those
/// need <see cref="ScadaBridgeAuditRedactor"/> with live options. Contract:
/// over-redact safely, never throw, never miss a header on the default
/// sensitive list.
/// </summary>
public sealed class SafeDefaultAuditRedactor : IAuditRedactor
{
/// <summary>Singleton instance — the redactor is stateless and side-effect-free.</summary>
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(
@"(?<name>[A-Za-z][A-Za-z0-9\-_]*)\s*:\s*(?<value>[^\r\n]*)",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private SafeDefaultAuditRedactor() { }
/// <summary>
/// Applies line-oriented header redaction to the default sensitive headers
/// (<c>Authorization</c>, <c>X-Api-Key</c>, <c>Cookie</c>, <c>Set-Cookie</c>)
/// found in <c>RequestSummary</c> and <c>ResponseSummary</c> inside
/// <paramref name="rawEvent"/>.<c>DetailsJson</c>. Never throws; over-redacts on
/// any internal failure.
/// </summary>
/// <param name="rawEvent">The audit event whose details JSON is to be redacted.</param>
/// <returns>A new <see cref="AuditEvent"/> with sensitive headers replaced by the redacted marker, or an over-redacted sentinel on failure.</returns>
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;
});
}
}