115 lines
3.8 KiB
C#
115 lines
3.8 KiB
C#
using System.Diagnostics;
|
|
using System.Text;
|
|
using Npgsql;
|
|
|
|
namespace StellaOps.Cli.Plugins.Aoc;
|
|
|
|
public interface IAocVerificationService
|
|
{
|
|
Task<AocVerificationResult> VerifyAsync(AocVerifyOptions options, CancellationToken cancellationToken);
|
|
}
|
|
|
|
public interface IAocConnectionFactory
|
|
{
|
|
ValueTask<NpgsqlConnection> OpenConnectionAsync(string connectionString, CancellationToken cancellationToken);
|
|
}
|
|
|
|
public sealed class NpgsqlConnectionFactory : IAocConnectionFactory
|
|
{
|
|
public async ValueTask<NpgsqlConnection> OpenConnectionAsync(string connectionString, CancellationToken cancellationToken)
|
|
{
|
|
var connection = new NpgsqlConnection(connectionString);
|
|
await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
|
|
return connection;
|
|
}
|
|
}
|
|
|
|
public sealed class AocVerificationService : IAocVerificationService
|
|
{
|
|
private readonly IAocConnectionFactory _connectionFactory;
|
|
private readonly TimeProvider _timeProvider;
|
|
|
|
public AocVerificationService(IAocConnectionFactory connectionFactory, TimeProvider timeProvider)
|
|
{
|
|
_connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory));
|
|
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
|
}
|
|
|
|
public async Task<AocVerificationResult> VerifyAsync(
|
|
AocVerifyOptions options,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var stopwatch = Stopwatch.StartNew();
|
|
var violations = new List<AocViolation>();
|
|
var documentsScanned = 0;
|
|
|
|
var query = AocVerificationQueryBuilder.Build(options);
|
|
await using var connection = await _connectionFactory.OpenConnectionAsync(
|
|
options.PostgresConnectionString,
|
|
cancellationToken).ConfigureAwait(false);
|
|
|
|
await using var cmd = new NpgsqlCommand(query.Sql, connection);
|
|
query.BindParameters(cmd);
|
|
|
|
await using var reader = await cmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
|
|
|
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
|
|
{
|
|
documentsScanned++;
|
|
|
|
_ = reader.GetString(0);
|
|
var hash = reader.IsDBNull(1) ? null : reader.GetString(1);
|
|
var previousHash = reader.IsDBNull(2) ? null : reader.GetString(2);
|
|
_ = reader.GetDateTime(3);
|
|
|
|
if (hash is null || previousHash is null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// TODO: implement hash chain verification and emit violations.
|
|
}
|
|
|
|
stopwatch.Stop();
|
|
|
|
return new AocVerificationResult
|
|
{
|
|
DocumentsScanned = documentsScanned,
|
|
ViolationCount = violations.Count,
|
|
Violations = violations,
|
|
DurationMs = stopwatch.ElapsedMilliseconds,
|
|
VerifiedAt = _timeProvider.GetUtcNow()
|
|
};
|
|
}
|
|
}
|
|
|
|
public readonly record struct AocVerificationQuery(string Sql, Action<NpgsqlCommand> BindParameters);
|
|
|
|
public static class AocVerificationQueryBuilder
|
|
{
|
|
public static AocVerificationQuery Build(AocVerifyOptions options)
|
|
{
|
|
var builder = new StringBuilder();
|
|
builder.AppendLine("SELECT id, hash, previous_hash, created_at");
|
|
builder.AppendLine("FROM aoc_documents");
|
|
builder.AppendLine("WHERE created_at >= @since");
|
|
|
|
if (!string.IsNullOrWhiteSpace(options.Tenant))
|
|
{
|
|
builder.AppendLine("AND tenant_id = @tenant");
|
|
}
|
|
|
|
builder.AppendLine("ORDER BY created_at ASC");
|
|
|
|
return new AocVerificationQuery(builder.ToString(), command =>
|
|
{
|
|
command.Parameters.AddWithValue("since", options.Since.UtcDateTime);
|
|
|
|
if (!string.IsNullOrWhiteSpace(options.Tenant))
|
|
{
|
|
command.Parameters.AddWithValue("tenant", options.Tenant);
|
|
}
|
|
});
|
|
}
|
|
}
|