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;
});
}
}