feat(audit): off-box chain-head export, verify-heads, maintenance login (ScadaBridge#107)

Item 1 (owner decision: no signing). AuditLog:HashChain:HeadExport (default
off): both centrals periodically read every month's chain head (LastSeq,
LastHash, purge anchor, chained rows, tombstones; one snapshot under the
chain lock SHARED) and append the changed heads to a sink the DB login cannot
write - File (append-only JSONL per node per day, local or UNC) or Syslog
(RFC 5424 over UDP / TCP octet counting). `audit verify-heads --path` reads
those records back (file or syslog-collector lines) and the new
VerifyAuditChainHeads command checks each against the database: Matches /
Extended / Purged, or Mismatch / Regressed / MissingHead / MissingLink /
CountMismatch (exit 3). AuditChainHeadStoreTests forges a chain with the
production hasher: verify-chain says Intact, verify-heads says Mismatch.

Item 3. AuditLog:MaintenanceConnectionString (${secret:}-capable, unset =
today's single login) runs the chain job, chain verify / head export, the
partition switch-out + archive and the per-channel purge on
AuditMaintenanceContext (keyed audit-maintenance repository/archive, purge
actor prefers them). AuditMaintenanceLoginTests runs the hardened setup on
SQL Server: writer (datareader/datawriter/audit_writer) ingests and is
refused chaining and deleting; the chainer+purger login does the full cycle.
Two-login grants documented in Component-AuditLog.md.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
Joseph Doherty
2026-11-09 23:16:05 -05:00
parent 5e7d5389d4
commit c4d7ec9b96
40 changed files with 2769 additions and 43 deletions
+14 -1
View File
@@ -304,7 +304,7 @@ The `scadabridge audit` group targets the centralized Audit Log component (#23)
exposes the UI-equivalent operational audit surface. Permissions follow the same
read-vs-export split the Central UI uses (see Component-AuditLog.md, Security &
Tamper-Evidence, and Security & Auth #10): `audit query`, `audit tree`, and
`audit verify-chain` and `audit archive list|verify` require the `OperationalAudit`
`audit verify-chain`, `audit verify-heads` and `audit archive list|verify` require the `OperationalAudit`
permission; `audit export`
additionally requires `AuditExport`; `audit backfill-source-node` requires the
`Admin` role (maintenance path only). The server enforces permission checks and
@@ -316,6 +316,7 @@ scadabridge audit export --since <t> --until <t> --format csv|jsonl|parquet --ou
scadabridge audit tree --execution-id <guid> [--format table|json]
scadabridge audit backfill-source-node --before <ISO-8601-UTC> [--sentinel <value>] [--batch <n>]
scadabridge audit verify-chain --month <YYYY-MM>
scadabridge audit verify-heads --path <file-or-directory>
scadabridge audit archive list
scadabridge audit archive verify --name <archive>
```
@@ -350,6 +351,18 @@ scadabridge audit archive verify --name <archive>
`0` intact or not chained, `3` broken, `1` error, `2` authorization failure. Requires
`OperationalAudit` and a caller not scoped to specific sites. See
Component-AuditLog.md, Security & Tamper-Evidence.
- `audit verify-heads --path <file-or-directory>` (ScadaBridge#107) — reads the AuditLog
chain heads exported off-box by `AuditLog:HashChain:HeadExport` (JSON lines from the file
sink, or any log a syslog collector wrote — each line's JSON object is taken from its first
`{` to its last `}`; a directory is read recursively for `*.jsonl`/`*.log`/`*.txt`),
collapses duplicates and sends them in the `VerifyAuditChainHeads` management command. The
server checks each exported head against the database chain — `Matches`, `Extended` (chain
grew; the link at the exported sequence still carries the exported hash), `Purged` (removed
by a partition purge; verify the archive), or the failures `Mismatch` (rewritten chain),
`Regressed`, `MissingHead`, `MissingLink`, `CountMismatch`. Exit `0` verified, `3` a
failure, `1` error (no records found, bad input, `CHAIN_BUSY`), `2` authorization failure.
Same gate as `verify-chain`. Run it together with `verify-chain`: a forger who rewrites rows
and re-hashes the chain inside SQL Server passes `verify-chain` but not `verify-heads`.
- `audit archive list` / `audit archive verify --name <archive>` (ScadaBridge#42) —
send the `ListAuditArchives` / `VerifyAuditArchive` management commands for the
AuditLog Parquet cold archives the retention purge writes (`AuditLog:Archive`,