chore(audit): ScadaBridge C7 — perf re-baseline + CollapseAuditLogToCanonical projection test + index-test fix + dead-cref cleanup (Task 2.5)

Perf re-baseline (HotPathLatencyTests): empirical p95 on Apple M-series Release
build: 4KB DetailsJson slow path ≈14 µs, small-DetailsJson no-redactors ≈2 µs,
true no-op fast path ≈0 µs. Thresholds updated: 200 µs / 30 µs / 5 µs (≈15×
headroom for contested CI runners). Old thresholds (50 µs / 10 µs) were set for
the pre-C3 typed-field path; canonical JSON parse+rewrite is empirically faster.
Adds a third test (Filter_Apply_NoDetailsJson_FastPath) that asserts same-instance
return on the DetailsJson-null + within-cap fast path. Env-var overrides retained.

CollapseAuditLogToCanonicalMigrationTests (new): three MSSQL-gated [SkippableFact]
tests verifying Action/Category/Outcome projection, NULL Actor, DetailsJson codec
round-trip, and all six persisted computed columns (Kind/Status/SourceSiteId/
ExecutionId/ParentExecutionId) for ApiOutbound, InboundAuthFailure, and Failed-
status rows.

AddAuditLogTableMigrationTests: rename CreatesFiveNamedIndexes →
CreatesNineNamedIndexes; expand coverage from 5 original indexes to all 9 named
non-clustered indexes present after CollapseAuditLogToCanonical (adds
IX_AuditLog_Execution, IX_AuditLog_ParentExecution, IX_AuditLog_Node_Occurred,
UX_AuditLog_EventId).

Dead-cref cleanup: zero references to the deleted IAuditPayloadFilter /
DefaultAuditPayloadFilter / SafeDefaultAuditPayloadFilter types remain in any
.cs file (source or test). 26 occurrences across 13 files replaced with correct
references to IAuditRedactor / ScadaBridgeAuditRedactor / SafeDefaultAuditRedactor
or reworded as plain prose.

Residual sweep: no unused transitional code found beyond the acknowledged
"C3 transitional shim" comments on IngestedAtUtc stamping (active code, not dead).
This commit is contained in:
Joseph Doherty
2026-06-02 14:59:23 -04:00
parent 68a6bd1720
commit 635461c0fd
20 changed files with 525 additions and 141 deletions
@@ -7,10 +7,9 @@ using static ZB.MOM.WW.ScadaBridge.AuditLog.Payload.AuditRedactionPrimitives;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Redaction;
/// <summary>
/// Canonical-record analogue of <see cref="SafeDefaultAuditPayloadFilter"/> for
/// stage C2 (Task 2.5): a minimal always-safe <see cref="IAuditRedactor"/>
/// fallback for composition roots that bypass the full
/// <see cref="ScadaBridgeAuditRedactor"/>. Performs line-oriented HTTP header
/// 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
@@ -11,23 +11,17 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Redaction;
/// <summary>
/// Canonical <see cref="IAuditRedactor"/> implementation for ScadaBridge — the
/// stage-C2 port of <see cref="DefaultAuditPayloadFilter"/> onto
/// <c>ZB.MOM.WW.Audit.AuditEvent</c> and its <see cref="AuditEvent.DetailsJson"/>
/// Canonical <see cref="IAuditRedactor"/> implementation for ScadaBridge —
/// operates on <c>ZB.MOM.WW.Audit.AuditEvent</c> and its <see cref="AuditEvent.DetailsJson"/>
/// payload bag. The ScadaBridge request/response/error/extra summaries travel
/// inside <c>DetailsJson</c> as a <see cref="AuditDetails"/> record (serialized
/// by <see cref="AuditDetailsCodec"/>); this redactor deserializes them, applies
/// the SAME header → body-regex → SQL-parameter → byte-safe truncation pipeline
/// the legacy filter applies, re-serializes, and returns a filtered COPY.
/// the header → body-regex → SQL-parameter → byte-safe truncation pipeline,
/// re-serializes, and returns a filtered COPY.
/// </summary>
/// <remarks>
/// <para>
/// Additive only: the legacy <see cref="IAuditPayloadFilter"/> pipeline stays in
/// place and wired until stage C3 swaps the record type; this redactor is the
/// canonical-record analogue exercised in isolation by the C2 unit tests.
/// </para>
/// <para>
/// Cap selection is faithful to the legacy filter, translated onto canonical
/// Cap selection is faithful to the original pipeline, translated onto canonical
/// fields:
/// <list type="bullet">
/// <item>The <c>ApiInbound</c> branch keys on <see cref="AuditEvent.Category"/>
@@ -49,7 +43,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Redaction;
/// <para>
/// MUST NOT throw — wrapped in try/catch; over-redacts (drops ALL sensitive free-text
/// fields to a safe marker) on any internal failure, mirroring
/// <see cref="SafeDefaultAuditPayloadFilter"/>.
/// <see cref="SafeDefaultAuditRedactor"/>.
/// </para>
/// </remarks>
public sealed class ScadaBridgeAuditRedactor : IAuditRedactor
@@ -238,8 +232,7 @@ public sealed class ScadaBridgeAuditRedactor : IAuditRedactor
/// <summary>
/// Combine the global and per-target body-redactor lists, returning the
/// compiled-regex set to apply. Patterns that failed compilation are
/// silently skipped. Identical resolution to
/// <see cref="DefaultAuditPayloadFilter"/>.
/// silently skipped.
/// </summary>
private IReadOnlyList<Regex> ResolveBodyRegexes(AuditLogOptions opts, string? target)
{
@@ -283,7 +276,6 @@ public sealed class ScadaBridgeAuditRedactor : IAuditRedactor
/// Resolve the per-connection SQL parameter redaction regex for the given
/// target. Connection key = everything before the first <c>.</c> in
/// <paramref name="target"/>. Patterns are forced case-insensitive.
/// Identical resolution to <see cref="DefaultAuditPayloadFilter"/>.
/// </summary>
private bool TryGetSqlParamRedactor(AuditLogOptions opts, string? target, out Regex? regex)
{