# Audit (who-did-what) Status: **Draft**. Normalized component — path to shared code. Goal: converge the three sister projects onto a canonical `AuditEvent` record + `AuditOutcome` enum + two thin seams (`IAuditWriter`, `IAuditRedactor`), proposed as the `ZB.MOM.WW.Audit` library, while each project keeps its own transport, storage, domain vocabulary, and redaction policy. - The one target: [`spec/SPEC.md`](spec/SPEC.md) - Canonical event model + field reference: [`spec/EVENT-MODEL.md`](spec/EVENT-MODEL.md) - The proposed shared library: [`shared-contract/ZB.MOM.WW.Audit.md`](shared-contract/ZB.MOM.WW.Audit.md) - Divergences + backlog: [`GAPS.md`](GAPS.md) - Current state, per project: [`current-state/`](current-state/) ## Why audit is a strong normalization candidate All three projects record a structured who-did-what trail with an actor identity, an action verb, and a timestamp. Two (OtOpcUa + ScadaBridge) already have a named `AuditEvent` record with an `EventId` idempotency key, `Actor`, and `CorrelationId`. ScadaBridge already ships **both** canonical seams under slightly different names (`IAuditWriter` is byte-for-byte the spec; `IAuditPayloadFilter` is the canonical `IAuditRedactor`). OtOpcUa's record is almost field-for-field aligned. MxGateway has a narrow API-key-lifecycle log that maps cleanly. The one new field across all three is `AuditOutcome` — no project stores it explicitly today; each encodes it implicitly and derives it at adoption. This is the bulk of the per-project work. Transport, storage, domain vocabulary, and redaction policy are **not** unified — each project keeps its own bespoke implementation behind the seam. **Audit closes the loop on Auth.** Every audit row's `Actor` is exactly the identity that the `ZB.MOM.WW.Auth` component normalizes (LDAP/GLAuth principal, API-key name). The library keeps `Actor` as a plain `string` (no Auth dependency), but at adoption each emit site supplies the Auth principal. **`IAuditRedactor` naming is aligned with Telemetry's `ILogRedactor`** — same shape and naming discipline so a future `ZB.MOM.WW.Hosting` aggregator wires both redactors with one mental model — but there is no cross-package dependency between the two libraries. ## Status by project | Project | Audit today | Seams present | `AuditOutcome` | Adoption status | |---|---|---|---|---| | **OtOpcUa** | Akka cluster-broadcast `AuditEvent` → cluster-singleton `AuditWriterActor` (batch 500/5 s, two-layer dedup) over EF `ConfigAuditLog` (SQL Server). Also a legacy SQL stored-procedure write path (bare `EventType`, NULL `EventId`). Admin UI page `ClusterAudit.razor`. | No named `IAuditWriter` seam; no redactor seam. | Not stored — encoded in `EventType` strings (`OpcUaAccessDenied`/`CrossClusterNamespaceAttempt` → `Denied`; config-write verbs → `Success`). | Not started | | **MxAccessGateway** | Single SQLite-backed `IApiKeyAuditStore` / `ApiKeyAuditEntry` — key lifecycle (CLI + dashboard) + constraint denials only. No authn events persisted; no production read consumer. | Narrow custom seam (`IApiKeyAuditStore`); no general `IAuditWriter`; redaction is by-construction (secret never enters the record type). | Not stored — derived: `constraint-denied` → `Denied`; all others → `Success`. | Not started | | **ScadaBridge** | Full pipeline: site SQLite hot-path (`SqliteAuditWriter` + ring-buffer fallback) → Akka `ClusterClient` forwarder → central MS SQL (ingest / reconcile / purge / partition maintenance). Rich ~25-field `AuditEvent` record. CLI `export`/`verify-chain`; Blazor audit UI. | ✅ `IAuditWriter` (matches canonical contract word-for-word); ✅ `IAuditPayloadFilter` (= canonical `IAuditRedactor`, identical signature, pure/never-throws/over-redacts). | Not stored explicitly — derived from `Status` (`Delivered`→`Success`; `Failed`/`Parked`/`Discarded`→`Failure`; `Kind = InboundAuthFailure`→`Denied`). | Not started (align, don't replace) | See each project's `current-state//CURRENT-STATE.md` for code-verified detail and adoption plan: - [`current-state/otopcua/CURRENT-STATE.md`](current-state/otopcua/CURRENT-STATE.md) - [`current-state/mxaccessgw/CURRENT-STATE.md`](current-state/mxaccessgw/CURRENT-STATE.md) - [`current-state/scadabridge/CURRENT-STATE.md`](current-state/scadabridge/CURRENT-STATE.md) ## Normalized vs. left per-project **Normalized (the shared `ZB.MOM.WW.Audit` library):** the canonical `AuditEvent` record (5 required fields + 4 optional common + `DetailsJson` extension bag); the `AuditOutcome` enum (`Success | Failure | Denied`); the `IAuditWriter` seam (best-effort, never throws to caller); the `IAuditRedactor` seam (pure, never throws, over-redacts on failure); shipped helpers (`NoOpAuditWriter`, `CompositeAuditWriter`, `RedactingAuditWriter`, `NullAuditRedactor`, `TruncatingAuditRedactor`). Library has no Akka / EF / SQLite / Serilog dependency; its only non-BCL dependency is `Microsoft.Extensions.DependencyInjection.Abstractions`. **Left per-project (each project keeps these behind the seam):** transport and storage (Akka singleton + EF/SQL Server; SQLite; site-SQLite + central MS SQL + forwarding/reconcile pipeline); domain vocabulary (`EventType` strings / API-key event-type literals / `Channel` + `Kind` + `Status` enums); query, CLI, and UI surfaces (`ClusterAudit.razor`; `ListRecentAsync`; `export` / `verify-chain`; Blazor audit pages); redaction *policy* (which fields/payloads are sensitive — only the `IAuditRedactor` *seam* is shared). > **Adoption is deferred this round.** The `ZB.MOM.WW.Audit` library is being designed and > the shared contract defined, but none of the three apps wire it in yet — exactly where > `ZB.MOM.WW.Auth` and `ZB.MOM.WW.Theme` sit today. The per-project adoption backlog is in > [`GAPS.md`](GAPS.md).