prep docs and service updates
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

This commit is contained in:
master
2025-11-21 06:56:36 +00:00
parent ca35db9ef4
commit d519782a8f
242 changed files with 17293 additions and 13367 deletions

View File

@@ -0,0 +1,266 @@
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Npgsql;
using NpgsqlTypes;
using StellaOps.Findings.Ledger.Infrastructure.Exports;
using StellaOps.Findings.Ledger.Infrastructure.Postgres;
using StellaOps.Findings.Ledger.WebService.Contracts;
namespace StellaOps.Findings.Ledger.WebService.Services;
/// <summary>
/// Provides deterministic paging helpers and SQL-backed queries for attestation verifications.
/// </summary>
public sealed class AttestationQueryService
{
private const int DefaultLimit = 200;
private const int MaxLimit = 1000;
private readonly LedgerDataSource? _dataSource;
private readonly ILogger<AttestationQueryService> _logger;
public AttestationQueryService(ILogger<AttestationQueryService> logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public AttestationQueryService(LedgerDataSource dataSource, ILogger<AttestationQueryService> logger)
{
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public int ClampLimit(int? requested)
{
if (!requested.HasValue || requested.Value <= 0)
{
return DefaultLimit;
}
return Math.Min(requested.Value, MaxLimit);
}
public string ComputeFiltersHash(AttestationQueryRequest request)
{
var filters = new Dictionary<string, string?>
{
["artifact_id"] = request.ArtifactId,
["finding_id"] = request.FindingId,
["attestation_id"] = request.AttestationId,
["status"] = request.Status,
["since_recorded_at"] = request.SinceRecordedAt?.ToString("O"),
["until_recorded_at"] = request.UntilRecordedAt?.ToString("O"),
["limit"] = request.Limit.ToString()
};
return ExportPaging.ComputeFiltersHash(filters);
}
public bool TryParsePageToken(string token, string expectedFiltersHash, out AttestationPagingKey? key, out string? error)
{
key = null;
error = null;
var base64 = token.Replace('-', '+').Replace('_', '/');
while (base64.Length % 4 != 0)
{
base64 += '=';
}
byte[] decodedBytes;
try
{
decodedBytes = Convert.FromBase64String(base64);
}
catch (FormatException)
{
error = "invalid_page_token_encoding";
return false;
}
AttestationPageToken? payload;
try
{
payload = JsonSerializer.Deserialize<AttestationPageToken>(decodedBytes);
}
catch (JsonException)
{
error = "invalid_page_token_payload";
return false;
}
if (payload is null || payload.Last is null)
{
error = "invalid_page_token_payload";
return false;
}
if (!string.Equals(payload.FiltersHash, expectedFiltersHash, StringComparison.Ordinal))
{
error = "page_token_filters_mismatch";
return false;
}
if (!DateTimeOffset.TryParse(payload.Last.RecordedAt, out var recordedAt))
{
error = "invalid_page_token_payload";
return false;
}
key = new AttestationPagingKey(recordedAt, payload.Last.AttestationId);
return true;
}
public string CreatePageToken(AttestationPagingKey key, string filtersHash)
{
var payload = new AttestationPageToken
{
FiltersHash = filtersHash,
Last = new AttestationPageKey
{
RecordedAt = key.RecordedAt.ToString("O"),
AttestationId = key.AttestationId
}
};
var json = JsonSerializer.Serialize(payload);
return Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(json))
.TrimEnd('=')
.Replace('+', '-')
.Replace('/', '_');
}
public async Task<ExportPage<AttestationExportItem>> GetAttestationsAsync(AttestationQueryRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
if (_dataSource is null)
{
throw new InvalidOperationException("data_source_unavailable");
}
if (!string.Equals(request.FiltersHash, ComputeFiltersHash(request), StringComparison.Ordinal))
{
throw new InvalidOperationException("filters_hash_mismatch");
}
const string baseSql = """
SELECT attestation_id,
artifact_id,
finding_id,
verification_status,
verification_time,
dsse_digest,
rekor_entry_id,
evidence_bundle_ref,
ledger_event_id,
recorded_at,
merkle_leaf_hash,
root_hash
FROM ledger_attestations
WHERE tenant_id = @tenant_id
""";
var sqlBuilder = new StringBuilder(baseSql);
var parameters = new List<NpgsqlParameter>
{
new("tenant_id", request.TenantId) { NpgsqlDbType = NpgsqlDbType.Text }
};
if (!string.IsNullOrWhiteSpace(request.ArtifactId))
{
sqlBuilder.Append(" AND artifact_id = @artifact_id");
parameters.Add(new NpgsqlParameter<string>("artifact_id", request.ArtifactId) { NpgsqlDbType = NpgsqlDbType.Text });
}
if (!string.IsNullOrWhiteSpace(request.FindingId))
{
sqlBuilder.Append(" AND finding_id = @finding_id");
parameters.Add(new NpgsqlParameter<string>("finding_id", request.FindingId) { NpgsqlDbType = NpgsqlDbType.Text });
}
if (!string.IsNullOrWhiteSpace(request.AttestationId))
{
sqlBuilder.Append(" AND attestation_id = @attestation_id");
parameters.Add(new NpgsqlParameter<string>("attestation_id", request.AttestationId) { NpgsqlDbType = NpgsqlDbType.Text });
}
if (!string.IsNullOrWhiteSpace(request.Status))
{
sqlBuilder.Append(" AND verification_status = @status");
parameters.Add(new NpgsqlParameter<string>("status", request.Status) { NpgsqlDbType = NpgsqlDbType.Text });
}
if (request.SinceRecordedAt.HasValue)
{
sqlBuilder.Append(" AND recorded_at >= @since_recorded_at");
parameters.Add(new NpgsqlParameter<DateTimeOffset>("since_recorded_at", request.SinceRecordedAt.Value) { NpgsqlDbType = NpgsqlDbType.TimestampTz });
}
if (request.UntilRecordedAt.HasValue)
{
sqlBuilder.Append(" AND recorded_at <= @until_recorded_at");
parameters.Add(new NpgsqlParameter<DateTimeOffset>("until_recorded_at", request.UntilRecordedAt.Value) { NpgsqlDbType = NpgsqlDbType.TimestampTz });
}
if (request.PagingKey is not null)
{
sqlBuilder.Append(" AND (recorded_at > @cursor_recorded_at OR (recorded_at = @cursor_recorded_at AND attestation_id > @cursor_attestation_id))");
parameters.Add(new NpgsqlParameter<DateTimeOffset>("cursor_recorded_at", request.PagingKey.RecordedAt) { NpgsqlDbType = NpgsqlDbType.TimestampTz });
parameters.Add(new NpgsqlParameter<string>("cursor_attestation_id", request.PagingKey.AttestationId) { NpgsqlDbType = NpgsqlDbType.Text });
}
sqlBuilder.Append(" ORDER BY recorded_at ASC, attestation_id ASC");
sqlBuilder.Append(" LIMIT @take");
parameters.Add(new NpgsqlParameter<int>("take", request.Limit + 1) { NpgsqlDbType = NpgsqlDbType.Integer });
await using var connection = await _dataSource.OpenConnectionAsync(request.TenantId, cancellationToken).ConfigureAwait(false);
await using var command = new NpgsqlCommand(sqlBuilder.ToString(), connection)
{
CommandTimeout = _dataSource.CommandTimeoutSeconds
};
command.Parameters.AddRange(parameters.ToArray());
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
var items = new List<AttestationExportItem>();
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
items.Add(new AttestationExportItem(
AttestationId: reader.GetString(0),
ArtifactId: reader.GetString(1),
FindingId: reader.IsDBNull(2) ? null : reader.GetString(2),
VerificationStatus: reader.GetString(3),
VerificationTime: reader.GetFieldValue<DateTimeOffset>(4),
DsseDigest: reader.GetString(5),
RekorEntryId: reader.IsDBNull(6) ? null : reader.GetString(6),
EvidenceBundleRef: reader.IsDBNull(7) ? null : reader.GetString(7),
LedgerEventId: reader.GetGuid(8).ToString(),
RecordedAt: reader.GetFieldValue<DateTimeOffset>(9),
MerkleLeafHash: reader.GetString(10),
RootHash: reader.GetString(11)));
}
string? nextPageToken = null;
if (items.Count > request.Limit)
{
var last = items[request.Limit];
items = items.Take(request.Limit).ToList();
var key = new AttestationPagingKey(last.RecordedAt, last.AttestationId);
nextPageToken = CreatePageToken(key, request.FiltersHash);
}
return new ExportPage<AttestationExportItem>(items, nextPageToken);
}
private sealed class AttestationPageToken
{
public string FiltersHash { get; set; } = string.Empty;
public AttestationPageKey? Last { get; set; }
}
private sealed class AttestationPageKey
{
public string RecordedAt { get; set; } = string.Empty;
public string AttestationId { get; set; } = string.Empty;
}
}