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
@@ -36,14 +36,15 @@ public sealed class FallbackAuditWriter : IAuditWriter
private readonly SemaphoreSlim _drainGate = new(1, 1);
/// <summary>
/// Bundle C (M5-T6) wires the singleton <see cref="IAuditPayloadFilter"/>
/// Bundle C (M5-T6) wires the singleton <see cref="IAuditRedactor"/>
/// here so every event written via the site hot path is truncated +
/// header/body/SQL-param redacted before it hits both the primary SQLite
/// writer AND the ring fallback. The parameter is optional (defaults to
/// no filtering) so the long tail of test composition roots that don't
/// care about the filter need no change — the production
/// the always-safe <see cref="SafeDefaultAuditRedactor"/>) so the long
/// tail of test composition roots that don't care about the redactor need
/// no change — the production
/// <see cref="ServiceCollectionExtensions.AddAuditLog"/> registration
/// always passes the real filter through.
/// always passes the real redactor through.
/// </summary>
/// <param name="primary">The primary audit writer (typically the SQLite writer).</param>
/// <param name="ring">Drop-oldest ring buffer used to stash events when the primary fails.</param>
@@ -62,14 +63,13 @@ public sealed class FallbackAuditWriter : IAuditWriter
_failureCounter = failureCounter ?? throw new ArgumentNullException(nameof(failureCounter));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
// AuditLog-008: never default to a null redactor — over-redact instead.
// C3 (Task 2.5): the canonical IAuditRedactor replaces the legacy
// IAuditPayloadFilter. SafeDefaultAuditRedactor performs HTTP header
// redaction with the hard-coded sensitive defaults (Authorization,
// X-Api-Key, Cookie, Set-Cookie) on the DetailsJson summaries so a test
// composition root that doesn't bind the real options never persists
// those headers verbatim. The full ScadaBridgeAuditRedactor (truncation
// + body / SQL-param redaction) is wired by AddAuditLog and takes
// precedence.
// C3 (Task 2.5): wired via the canonical IAuditRedactor seam.
// SafeDefaultAuditRedactor performs HTTP header redaction with the
// hard-coded sensitive defaults (Authorization, X-Api-Key, Cookie,
// Set-Cookie) on the DetailsJson summaries so a test composition root
// that doesn't bind the real options never persists those headers
// verbatim. The full ScadaBridgeAuditRedactor (truncation + body /
// SQL-param redaction) is wired by AddAuditLog and takes precedence.
_redactor = redactor ?? SafeDefaultAuditRedactor.Instance;
}
@@ -6,10 +6,10 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Site;
/// <summary>
/// Audit Log (#23) M5 Bundle C — bridges
/// <see cref="IAuditRedactionFailureCounter"/> (incremented by
/// <see cref="DefaultAuditPayloadFilter"/> every time a header / body / SQL
/// parameter redactor stage throws and the filter has to over-redact the
/// offending field) into <see cref="ISiteHealthCollector"/> so the count
/// surfaces in the site health report payload as
/// <see cref="ZB.MOM.WW.ScadaBridge.AuditLog.Redaction.ScadaBridgeAuditRedactor"/> every time
/// a header / body / SQL parameter redactor stage throws and the redactor has
/// to over-redact the offending field) into <see cref="ISiteHealthCollector"/>
/// so the count surfaces in the site health report payload as
/// <c>SiteHealthReport.AuditRedactionFailure</c>.
/// </summary>
/// <remarks>