Files
ScadaBridge/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/IAuditLogHashChain.cs
T
Joseph Doherty fd4be80913 feat(audit): SHA-256 hash chain over the central AuditLog (ScadaBridge#41)
Audit roadmap T1 (alog.md §11.4): one chain per UTC calendar month of
OccurredAtUtc. Each chained row carries ChainSeq (gap-free within its month)
and RowHash = SHA-256(prev.RowHash || canonical(row)); the v1 canonical form
(AuditChainHasher) is pinned by a known-answer vector computed independently.

- Migration AddAuditLogHashChain: nullable ChainSeq/RowHash on dbo.AuditLog
  (metadata-only add), AuditLogChainHead (per-month chain end + partition-purge
  checkpoint anchor), AuditLogChainTombstone, two partition-aligned filtered
  indexes, scadabridge_audit_chainer role with a column-level UPDATE grant.
  Production script docs/plans/sql/AddAuditLogHashChain.sql.
- AuditLogHashChain chains unchained rows oldest month first under an
  exclusive sp_getapplock (zero timeout, so a dual-active central pair still
  yields one chain), with an optimistic head check and a ChainSeq IS NULL
  row-count check that roll a pass back instead of leaving a gap. The ingest
  path is untouched.
- Purges keep the chain verifiable: the partition switch records the month's
  checkpoint anchor and drops its tombstones; the per-channel DELETE writes a
  tombstone per chained row in the same transaction (OUTPUT INTO). Both, and
  the SourceNode backfill, hold the chain lock; the backfill skips chained
  rows. The switch staging table gains the two new columns.
- AuditChainVerifier reports the first break (partition, ChainSeq, EventId,
  kind) for edited, deleted, reordered rows and tail truncation.
- AuditLogHashChainService (central, default OFF via AuditLog:HashChain).

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 16:33:11 -05:00

47 lines
2.3 KiB
C#

using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
/// <summary>
/// Hash-chain tamper evidence for the central <c>AuditLog</c> table (audit roadmap T1,
/// alog.md §11.4, ScadaBridge#41).
/// </summary>
/// <remarks>
/// <para>
/// One chain per <b>partition</b> = UTC calendar month of <c>OccurredAtUtc</c> (the same
/// month the <c>ps_AuditLog_Month</c> scheme partitions on). Each chained row carries a
/// <c>ChainSeq</c> (1, 2, 3 … within its month) and
/// <c>RowHash = SHA-256(prev.RowHash || canonical(row))</c>, where <c>prev</c> is the row
/// at <c>ChainSeq - 1</c> (32 zero bytes for the first). The canonical serialisation
/// covers every writable column plus the month and sequence number.
/// </para>
/// <para>
/// Rows are linked by a <b>chaining job</b>, not by the hot-path writers: two central
/// nodes and the site-ingest path all INSERT concurrently, so a write-time chain would
/// need a global serialisation point on every audit write. The job serialises itself
/// with an exclusive SQL Server application lock, so even a dual-active central pair
/// produces one gap-free chain per month.
/// </para>
/// </remarks>
public interface IAuditLogHashChain
{
/// <summary>
/// Links up to <paramref name="batchSize"/> unchained rows (oldest
/// <c>OccurredAtUtc</c> first) onto the end of their month's chain, in one transaction.
/// Returns immediately with <see cref="AuditChainBatchResult.LockBusy"/> set when another
/// chainer holds the chain lock.
/// </summary>
/// <param name="batchSize">Maximum rows to chain in this pass.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>How many rows were chained, and whether the lock was busy.</returns>
Task<AuditChainBatchResult> ChainPendingAsync(int batchSize, CancellationToken ct = default);
/// <summary>
/// Verifies one month's chain end to end and reports the first break.
/// </summary>
/// <param name="monthStartUtc">The first instant of the month (UTC, day 1, midnight).</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The verification result.</returns>
Task<AuditChainVerificationResult> VerifyMonthAsync(DateTime monthStartUtc, CancellationToken ct = default);
}