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
This commit is contained in:
+5
-2
@@ -97,8 +97,9 @@ public class AuditLogEntityTypeConfigurationTests : IDisposable
|
||||
// C5: 10 canonical columns (EventId, OccurredAtUtc, Actor, Action, Outcome,
|
||||
// Channel[=Category], Target, SourceNode, CorrelationId, DetailsJson) + 6
|
||||
// persisted computed columns (Kind, Status, SourceSiteId, ExecutionId,
|
||||
// ParentExecutionId, IngestedAtUtc) = 16.
|
||||
Assert.Equal(16, properties.Count);
|
||||
// ParentExecutionId, IngestedAtUtc) + 2 hash-chain link columns (ChainSeq,
|
||||
// RowHash — ScadaBridge#41) = 18.
|
||||
Assert.Equal(18, properties.Count);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the channel property maps to the canonical category column.</summary>
|
||||
@@ -177,6 +178,7 @@ public class AuditLogEntityTypeConfigurationTests : IDisposable
|
||||
var expected = new[]
|
||||
{
|
||||
"IX_AuditLog_Actor_Occurred",
|
||||
"IX_AuditLog_ChainSeq", // hash chain (ScadaBridge#41)
|
||||
"IX_AuditLog_Channel_Status_Occurred",
|
||||
"IX_AuditLog_CorrelationId",
|
||||
"IX_AuditLog_Execution",
|
||||
@@ -186,6 +188,7 @@ public class AuditLogEntityTypeConfigurationTests : IDisposable
|
||||
"IX_AuditLog_ParentExecution",
|
||||
"IX_AuditLog_Site_Occurred",
|
||||
"IX_AuditLog_Target_Occurred",
|
||||
"IX_AuditLog_Unchained", // hash chain (ScadaBridge#41)
|
||||
// UX_AuditLog_EventId is intentionally absent — dropped by
|
||||
// AlignAuditLogEventIdUniqueness; the aligned clustered PK carries it.
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user