73 lines
5.7 KiB
Markdown
73 lines
5.7 KiB
Markdown
# 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).
|