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:
@@ -52,6 +52,12 @@ public static class ServiceCollectionExtensions
|
||||
/// <summary>Configuration section bound to <see cref="AuditLogPartitionMaintenanceOptions"/>.</summary>
|
||||
public const string PartitionMaintenanceSectionName = "AuditLog:PartitionMaintenance";
|
||||
|
||||
/// <summary>
|
||||
/// Optional connection string for the AuditLog maintenance paths — the hash-chain job and both
|
||||
/// purge paths (ScadaBridge#107 item 3). Unset: they run on <c>ScadaBridge:Database:ConfigurationDb</c>.
|
||||
/// </summary>
|
||||
public const string MaintenanceConnectionStringKey = "AuditLog:MaintenanceConnectionString";
|
||||
|
||||
/// <summary>Configuration section bound to <see cref="AuditLogHashChainOptions"/> (ScadaBridge#41).</summary>
|
||||
public const string HashChainSectionName = "AuditLog:HashChain";
|
||||
|
||||
@@ -411,6 +417,26 @@ public static class ServiceCollectionExtensions
|
||||
config, HashChainSectionName);
|
||||
services.AddHostedService<AuditLogHashChainService>();
|
||||
|
||||
// Off-box chain-head export (ScadaBridge#107 item 1, AuditLog:HashChain:HeadExport,
|
||||
// default off). The sink is built from the bound options; null when disabled.
|
||||
services.AddHostedService(sp =>
|
||||
{
|
||||
var options = sp.GetRequiredService<Microsoft.Extensions.Options.IOptions<AuditLogHashChainOptions>>();
|
||||
var node = sp.GetService<INodeIdentityProvider>()?.NodeName;
|
||||
var export = options.Value.HeadExport;
|
||||
Central.HeadExport.IAuditChainHeadSink? sink = !export.Enabled ? null : export.Sink switch
|
||||
{
|
||||
Central.HeadExport.AuditChainHeadSinkKind.Syslog => new Central.HeadExport.SyslogAuditChainHeadSink(export.Syslog),
|
||||
_ => new Central.HeadExport.FileAuditChainHeadSink(export.File.Directory!, node ?? Environment.MachineName),
|
||||
};
|
||||
return new Central.HeadExport.AuditChainHeadExportService(
|
||||
sp.GetRequiredService<IServiceScopeFactory>(),
|
||||
options,
|
||||
sink,
|
||||
node,
|
||||
sp.GetRequiredService<ILogger<Central.HeadExport.AuditChainHeadExportService>>());
|
||||
});
|
||||
|
||||
// Parquet cold archive (ScadaBridge#42). Default OFF; when enabled the purge actor
|
||||
// archives each expired partition before switching it out. IAuditLogArchive is
|
||||
// registered by AddConfigurationDatabase (central only) and reads these options.
|
||||
|
||||
Reference in New Issue
Block a user