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
@@ -1,4 +1,5 @@
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
@@ -32,6 +33,15 @@ public record AuditLogQueryResult(IReadOnlyList<AuditLogEntry> Entries, int Tota
/// <param name="Month">The month to verify, <c>YYYY-MM</c>.</param>
public record VerifyAuditChainCommand(string Month);
/// <summary>
/// Checks AuditLog chain heads exported off-box (<c>AuditLog:HashChain:HeadExport</c>,
/// ScadaBridge#107 item 1) against the database chain: does the database still carry each
/// exported hash at its exported sequence? Read-only; same gate as
/// <see cref="VerifyAuditChainCommand"/>. The CLI reads the records from the export files.
/// </summary>
/// <param name="Records">The exported head records (at most 50,000; duplicates are collapsed).</param>
public record VerifyAuditChainHeadsCommand(IReadOnlyList<AuditChainHeadRecord> Records);
/// <summary>
/// Lists the AuditLog Parquet cold archives under the configured archive directory (audit
/// roadmap T2, ScadaBridge#42). Read-only; OperationalAudit roles, refused for a site-scoped