# 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/<project>/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).
