-- AddAuditLogHashChain (2026-11-09, ScadaBridge#41). AuditLog hash-chain tamper evidence: -- ChainSeq/RowHash on dbo.AuditLog, AuditLogChainHead + AuditLogChainTombstone, two partition-aligned -- filtered indexes, the scadabridge_audit_chainer role. Apply to the central configuration database -- with both central nodes stopped (old binaries cannot switch out a partition once the columns exist). BEGIN TRANSACTION; ALTER TABLE [AuditLog] ADD [ChainSeq] bigint NULL; ALTER TABLE [AuditLog] ADD [RowHash] binary(32) NULL; CREATE TABLE [AuditLogChainHead] ( [ChainMonth] date NOT NULL, [LastSeq] bigint NOT NULL, [LastHash] binary(32) NOT NULL, [PurgedThroughSeq] bigint NULL, [PurgedThroughHash] binary(32) NULL, [UpdatedAtUtc] datetime2 NOT NULL, CONSTRAINT [PK_AuditLogChainHead] PRIMARY KEY ([ChainMonth]) ); CREATE TABLE [AuditLogChainTombstone] ( [ChainMonth] date NOT NULL, [ChainSeq] bigint NOT NULL, [RowHash] binary(32) NOT NULL, [EventId] uniqueidentifier NOT NULL, [PurgedAtUtc] datetime2 NOT NULL, CONSTRAINT [PK_AuditLogChainTombstone] PRIMARY KEY ([ChainMonth], [ChainSeq]) ); CREATE NONCLUSTERED INDEX IX_AuditLog_Unchained ON dbo.AuditLog (OccurredAtUtc, EventId) WHERE ChainSeq IS NULL ON ps_AuditLog_Month(OccurredAtUtc); CREATE NONCLUSTERED INDEX IX_AuditLog_ChainSeq ON dbo.AuditLog (ChainSeq) WHERE ChainSeq IS NOT NULL ON ps_AuditLog_Month(OccurredAtUtc); IF DATABASE_PRINCIPAL_ID('scadabridge_audit_chainer') IS NULL EXEC sp_executesql N'CREATE ROLE scadabridge_audit_chainer'; -- The chaining job reads unchained rows and writes ONLY the two link columns. GRANT SELECT ON dbo.AuditLog TO scadabridge_audit_chainer; GRANT UPDATE (ChainSeq, RowHash) ON dbo.AuditLog TO scadabridge_audit_chainer; GRANT SELECT, INSERT, UPDATE ON dbo.AuditLogChainHead TO scadabridge_audit_chainer; GRANT SELECT ON dbo.AuditLogChainTombstone TO scadabridge_audit_chainer; IF DATABASE_PRINCIPAL_ID('scadabridge_audit_purger') IS NULL EXEC sp_executesql N'CREATE ROLE scadabridge_audit_purger'; -- The per-channel purge writes a tombstone per chained row it removes; the partition -- switch moves the month's head to its checkpoint anchor and drops its tombstones. GRANT SELECT, UPDATE ON dbo.AuditLogChainHead TO scadabridge_audit_purger; GRANT SELECT, INSERT, DELETE ON dbo.AuditLogChainTombstone TO scadabridge_audit_purger; IF DATABASE_PRINCIPAL_ID('scadabridge_audit_writer') IS NULL EXEC sp_executesql N'CREATE ROLE scadabridge_audit_writer'; -- The ingest principal must never be able to forge chain state. DENY INSERT, UPDATE, DELETE ON dbo.AuditLogChainHead TO scadabridge_audit_writer; DENY INSERT, UPDATE, DELETE ON dbo.AuditLogChainTombstone TO scadabridge_audit_writer; INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) VALUES (N'20261109211317_AddAuditLogHashChain', N'10.0.13'); COMMIT; GO