feat(audit): persist OPC UA certificate-trust decisions to the central audit log (ScadaBridge#43)

New AuditChannel.Security with AuditKind.CertTrust / CertTrustRemove (appended, ordinals stable).
Both central entry points - the management actor handlers and the Central UI CertManagementService -
write a best-effort ICentralAuditWriter row after the site outcome is known (Delivered when all
reachable site nodes acked, Failed otherwise, incl. relay timeouts), naming actor, site, thumbprint
and connection. Shared builder: CertTrustAuditEvents.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
Joseph Doherty
2026-11-09 15:17:03 -05:00
parent f98369a786
commit f5eed5e8fd
10 changed files with 322 additions and 22 deletions
@@ -5621,6 +5621,46 @@ public class ManagementActorTests : TestKit, IDisposable
Assert.Equal("SITE1", stub.LastTrustSite);
}
/// <summary>Gitea #43: an admin trust decision is persisted to the central audit log with the real actor and outcome.</summary>
[Fact]
public void TrustServerCert_Admin_WritesCentralAuditRow()
{
var writer = Substitute.For<ICentralAuditWriter>();
_services.AddSingleton(writer);
_services.AddSingleton<CommunicationService>(new StubCommunicationService());
var actor = CreateActor();
actor.Tell(Envelope(new TrustServerCertCommand("conn", "ZGVy", "AB12", "SITE1"), "Administrator"));
ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
var evt = Assert.Single(writer.ReceivedCalls()
.Where(c => c.GetMethodInfo().Name == nameof(ICentralAuditWriter.WriteAsync))
.Select(c => (ZB.MOM.WW.Audit.AuditEvent)c.GetArguments()[0]!));
Assert.Equal("Security", evt.Category);
Assert.Contains("\"kind\":\"CertTrust\"", evt.DetailsJson);
Assert.Contains("\"status\":\"Delivered\"", evt.DetailsJson);
Assert.Equal("SITE1/AB12", evt.Target);
Assert.False(string.IsNullOrEmpty(evt.Actor));
}
/// <summary>Gitea #43: a removal is audited too.</summary>
[Fact]
public void RemoveServerCert_Admin_WritesCentralAuditRow()
{
var writer = Substitute.For<ICentralAuditWriter>();
_services.AddSingleton(writer);
_services.AddSingleton<CommunicationService>(new StubCommunicationService());
var actor = CreateActor();
actor.Tell(Envelope(new RemoveServerCertCommand("AB12", "SITE1"), "Administrator"));
ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
var evt = Assert.Single(writer.ReceivedCalls()
.Where(c => c.GetMethodInfo().Name == nameof(ICentralAuditWriter.WriteAsync))
.Select(c => (ZB.MOM.WW.Audit.AuditEvent)c.GetArguments()[0]!));
Assert.Contains("\"kind\":\"CertTrustRemove\"", evt.DetailsJson);
}
/// <summary>Verifies that remove server cert, when admin, relays to site.</summary>
[Fact]
public void RemoveServerCert_Admin_RelaysToSite()