Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
- Implement comprehensive tests for PhpFrameworkSurface, covering scenarios such as empty surfaces, presence of routes, controllers, middlewares, CLI commands, cron jobs, and event listeners. - Validate metadata creation for route counts, HTTP methods, protected and public routes, and route patterns. - Introduce tests for PhpPharScanner, including handling of non-existent files, null or empty paths, invalid PHAR files, and minimal PHAR structures. - Ensure correct computation of SHA256 for valid PHAR files and validate the properties of PhpPharArchive, PhpPharEntry, and PhpPharScanResult.
403 lines
16 KiB
C#
403 lines
16 KiB
C#
namespace StellaOps.Findings.Ledger.Infrastructure.Postgres;
|
|
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using Npgsql;
|
|
using NpgsqlTypes;
|
|
using StellaOps.Findings.Ledger.Domain;
|
|
using StellaOps.Findings.Ledger.Infrastructure.Snapshot;
|
|
|
|
/// <summary>
|
|
/// PostgreSQL implementation of snapshot repository.
|
|
/// </summary>
|
|
public sealed class PostgresSnapshotRepository : ISnapshotRepository
|
|
{
|
|
private readonly NpgsqlDataSource _dataSource;
|
|
private readonly JsonSerializerOptions _jsonOptions;
|
|
|
|
public PostgresSnapshotRepository(NpgsqlDataSource dataSource)
|
|
{
|
|
_dataSource = dataSource;
|
|
_jsonOptions = new JsonSerializerOptions
|
|
{
|
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
WriteIndented = false
|
|
};
|
|
}
|
|
|
|
public async Task<LedgerSnapshot> CreateAsync(
|
|
string tenantId,
|
|
CreateSnapshotInput input,
|
|
long currentSequence,
|
|
DateTimeOffset currentTimestamp,
|
|
CancellationToken ct = default)
|
|
{
|
|
var snapshotId = Guid.NewGuid();
|
|
var createdAt = DateTimeOffset.UtcNow;
|
|
var expiresAt = input.ExpiresIn.HasValue
|
|
? createdAt.Add(input.ExpiresIn.Value)
|
|
: (DateTimeOffset?)null;
|
|
|
|
var sequenceNumber = input.AtSequence ?? currentSequence;
|
|
var timestamp = input.AtTimestamp ?? currentTimestamp;
|
|
|
|
var initialStats = new SnapshotStatistics(0, 0, 0, 0, 0, 0);
|
|
var metadataJson = input.Metadata != null
|
|
? JsonSerializer.Serialize(input.Metadata, _jsonOptions)
|
|
: null;
|
|
var entityTypesJson = input.IncludeEntityTypes != null
|
|
? JsonSerializer.Serialize(input.IncludeEntityTypes.Select(e => e.ToString()).ToList(), _jsonOptions)
|
|
: null;
|
|
|
|
const string sql = """
|
|
INSERT INTO ledger_snapshots (
|
|
tenant_id, snapshot_id, label, description, status,
|
|
created_at, expires_at, sequence_number, snapshot_timestamp,
|
|
findings_count, vex_statements_count, advisories_count,
|
|
sboms_count, events_count, size_bytes,
|
|
merkle_root, dsse_digest, metadata, include_entity_types, sign_requested
|
|
) VALUES (
|
|
@tenantId, @snapshotId, @label, @description, @status,
|
|
@createdAt, @expiresAt, @sequenceNumber, @timestamp,
|
|
@findingsCount, @vexCount, @advisoriesCount,
|
|
@sbomsCount, @eventsCount, @sizeBytes,
|
|
@merkleRoot, @dsseDigest, @metadata::jsonb, @entityTypes::jsonb, @sign
|
|
)
|
|
""";
|
|
|
|
await using var cmd = _dataSource.CreateCommand(sql);
|
|
cmd.Parameters.AddWithValue("tenantId", tenantId);
|
|
cmd.Parameters.AddWithValue("snapshotId", snapshotId);
|
|
cmd.Parameters.AddWithValue("label", (object?)input.Label ?? DBNull.Value);
|
|
cmd.Parameters.AddWithValue("description", (object?)input.Description ?? DBNull.Value);
|
|
cmd.Parameters.AddWithValue("status", SnapshotStatus.Creating.ToString());
|
|
cmd.Parameters.AddWithValue("createdAt", createdAt);
|
|
cmd.Parameters.AddWithValue("expiresAt", (object?)expiresAt ?? DBNull.Value);
|
|
cmd.Parameters.AddWithValue("sequenceNumber", sequenceNumber);
|
|
cmd.Parameters.AddWithValue("timestamp", timestamp);
|
|
cmd.Parameters.AddWithValue("findingsCount", initialStats.FindingsCount);
|
|
cmd.Parameters.AddWithValue("vexCount", initialStats.VexStatementsCount);
|
|
cmd.Parameters.AddWithValue("advisoriesCount", initialStats.AdvisoriesCount);
|
|
cmd.Parameters.AddWithValue("sbomsCount", initialStats.SbomsCount);
|
|
cmd.Parameters.AddWithValue("eventsCount", initialStats.EventsCount);
|
|
cmd.Parameters.AddWithValue("sizeBytes", initialStats.SizeBytes);
|
|
cmd.Parameters.AddWithValue("merkleRoot", DBNull.Value);
|
|
cmd.Parameters.AddWithValue("dsseDigest", DBNull.Value);
|
|
cmd.Parameters.AddWithValue("metadata", (object?)metadataJson ?? DBNull.Value);
|
|
cmd.Parameters.AddWithValue("entityTypes", (object?)entityTypesJson ?? DBNull.Value);
|
|
cmd.Parameters.AddWithValue("sign", input.Sign);
|
|
|
|
await cmd.ExecuteNonQueryAsync(ct);
|
|
|
|
return new LedgerSnapshot(
|
|
tenantId,
|
|
snapshotId,
|
|
input.Label,
|
|
input.Description,
|
|
SnapshotStatus.Creating,
|
|
createdAt,
|
|
expiresAt,
|
|
sequenceNumber,
|
|
timestamp,
|
|
initialStats,
|
|
null,
|
|
null,
|
|
input.Metadata);
|
|
}
|
|
|
|
public async Task<LedgerSnapshot?> GetByIdAsync(
|
|
string tenantId,
|
|
Guid snapshotId,
|
|
CancellationToken ct = default)
|
|
{
|
|
const string sql = """
|
|
SELECT tenant_id, snapshot_id, label, description, status,
|
|
created_at, expires_at, sequence_number, snapshot_timestamp,
|
|
findings_count, vex_statements_count, advisories_count,
|
|
sboms_count, events_count, size_bytes,
|
|
merkle_root, dsse_digest, metadata
|
|
FROM ledger_snapshots
|
|
WHERE tenant_id = @tenantId AND snapshot_id = @snapshotId
|
|
""";
|
|
|
|
await using var cmd = _dataSource.CreateCommand(sql);
|
|
cmd.Parameters.AddWithValue("tenantId", tenantId);
|
|
cmd.Parameters.AddWithValue("snapshotId", snapshotId);
|
|
|
|
await using var reader = await cmd.ExecuteReaderAsync(ct);
|
|
if (!await reader.ReadAsync(ct))
|
|
return null;
|
|
|
|
return MapSnapshot(reader);
|
|
}
|
|
|
|
public async Task<(IReadOnlyList<LedgerSnapshot> Snapshots, string? NextPageToken)> ListAsync(
|
|
SnapshotListQuery query,
|
|
CancellationToken ct = default)
|
|
{
|
|
var sql = new StringBuilder("""
|
|
SELECT tenant_id, snapshot_id, label, description, status,
|
|
created_at, expires_at, sequence_number, snapshot_timestamp,
|
|
findings_count, vex_statements_count, advisories_count,
|
|
sboms_count, events_count, size_bytes,
|
|
merkle_root, dsse_digest, metadata
|
|
FROM ledger_snapshots
|
|
WHERE tenant_id = @tenantId
|
|
""");
|
|
|
|
var parameters = new List<NpgsqlParameter>
|
|
{
|
|
new("tenantId", query.TenantId)
|
|
};
|
|
|
|
if (query.Status.HasValue)
|
|
{
|
|
sql.Append(" AND status = @status");
|
|
parameters.Add(new NpgsqlParameter("status", query.Status.Value.ToString()));
|
|
}
|
|
|
|
if (query.CreatedAfter.HasValue)
|
|
{
|
|
sql.Append(" AND created_at >= @createdAfter");
|
|
parameters.Add(new NpgsqlParameter("createdAfter", query.CreatedAfter.Value));
|
|
}
|
|
|
|
if (query.CreatedBefore.HasValue)
|
|
{
|
|
sql.Append(" AND created_at < @createdBefore");
|
|
parameters.Add(new NpgsqlParameter("createdBefore", query.CreatedBefore.Value));
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(query.PageToken))
|
|
{
|
|
if (Guid.TryParse(query.PageToken, out var lastId))
|
|
{
|
|
sql.Append(" AND snapshot_id > @lastId");
|
|
parameters.Add(new NpgsqlParameter("lastId", lastId));
|
|
}
|
|
}
|
|
|
|
sql.Append(" ORDER BY created_at DESC, snapshot_id");
|
|
sql.Append(" LIMIT @limit");
|
|
parameters.Add(new NpgsqlParameter("limit", query.PageSize + 1));
|
|
|
|
await using var cmd = _dataSource.CreateCommand(sql.ToString());
|
|
cmd.Parameters.AddRange(parameters.ToArray());
|
|
|
|
var snapshots = new List<LedgerSnapshot>();
|
|
await using var reader = await cmd.ExecuteReaderAsync(ct);
|
|
|
|
while (await reader.ReadAsync(ct) && snapshots.Count < query.PageSize)
|
|
{
|
|
snapshots.Add(MapSnapshot(reader));
|
|
}
|
|
|
|
string? nextPageToken = null;
|
|
if (await reader.ReadAsync(ct))
|
|
{
|
|
nextPageToken = snapshots.Last().SnapshotId.ToString();
|
|
}
|
|
|
|
return (snapshots, nextPageToken);
|
|
}
|
|
|
|
public async Task<bool> UpdateStatusAsync(
|
|
string tenantId,
|
|
Guid snapshotId,
|
|
SnapshotStatus newStatus,
|
|
CancellationToken ct = default)
|
|
{
|
|
const string sql = """
|
|
UPDATE ledger_snapshots
|
|
SET status = @status, updated_at = @updatedAt
|
|
WHERE tenant_id = @tenantId AND snapshot_id = @snapshotId
|
|
""";
|
|
|
|
await using var cmd = _dataSource.CreateCommand(sql);
|
|
cmd.Parameters.AddWithValue("tenantId", tenantId);
|
|
cmd.Parameters.AddWithValue("snapshotId", snapshotId);
|
|
cmd.Parameters.AddWithValue("status", newStatus.ToString());
|
|
cmd.Parameters.AddWithValue("updatedAt", DateTimeOffset.UtcNow);
|
|
|
|
return await cmd.ExecuteNonQueryAsync(ct) > 0;
|
|
}
|
|
|
|
public async Task<bool> UpdateStatisticsAsync(
|
|
string tenantId,
|
|
Guid snapshotId,
|
|
SnapshotStatistics statistics,
|
|
CancellationToken ct = default)
|
|
{
|
|
const string sql = """
|
|
UPDATE ledger_snapshots
|
|
SET findings_count = @findingsCount,
|
|
vex_statements_count = @vexCount,
|
|
advisories_count = @advisoriesCount,
|
|
sboms_count = @sbomsCount,
|
|
events_count = @eventsCount,
|
|
size_bytes = @sizeBytes,
|
|
updated_at = @updatedAt
|
|
WHERE tenant_id = @tenantId AND snapshot_id = @snapshotId
|
|
""";
|
|
|
|
await using var cmd = _dataSource.CreateCommand(sql);
|
|
cmd.Parameters.AddWithValue("tenantId", tenantId);
|
|
cmd.Parameters.AddWithValue("snapshotId", snapshotId);
|
|
cmd.Parameters.AddWithValue("findingsCount", statistics.FindingsCount);
|
|
cmd.Parameters.AddWithValue("vexCount", statistics.VexStatementsCount);
|
|
cmd.Parameters.AddWithValue("advisoriesCount", statistics.AdvisoriesCount);
|
|
cmd.Parameters.AddWithValue("sbomsCount", statistics.SbomsCount);
|
|
cmd.Parameters.AddWithValue("eventsCount", statistics.EventsCount);
|
|
cmd.Parameters.AddWithValue("sizeBytes", statistics.SizeBytes);
|
|
cmd.Parameters.AddWithValue("updatedAt", DateTimeOffset.UtcNow);
|
|
|
|
return await cmd.ExecuteNonQueryAsync(ct) > 0;
|
|
}
|
|
|
|
public async Task<bool> SetMerkleRootAsync(
|
|
string tenantId,
|
|
Guid snapshotId,
|
|
string merkleRoot,
|
|
string? dsseDigest,
|
|
CancellationToken ct = default)
|
|
{
|
|
const string sql = """
|
|
UPDATE ledger_snapshots
|
|
SET merkle_root = @merkleRoot,
|
|
dsse_digest = @dsseDigest,
|
|
updated_at = @updatedAt
|
|
WHERE tenant_id = @tenantId AND snapshot_id = @snapshotId
|
|
""";
|
|
|
|
await using var cmd = _dataSource.CreateCommand(sql);
|
|
cmd.Parameters.AddWithValue("tenantId", tenantId);
|
|
cmd.Parameters.AddWithValue("snapshotId", snapshotId);
|
|
cmd.Parameters.AddWithValue("merkleRoot", merkleRoot);
|
|
cmd.Parameters.AddWithValue("dsseDigest", (object?)dsseDigest ?? DBNull.Value);
|
|
cmd.Parameters.AddWithValue("updatedAt", DateTimeOffset.UtcNow);
|
|
|
|
return await cmd.ExecuteNonQueryAsync(ct) > 0;
|
|
}
|
|
|
|
public async Task<int> ExpireSnapshotsAsync(
|
|
DateTimeOffset cutoff,
|
|
CancellationToken ct = default)
|
|
{
|
|
const string sql = """
|
|
UPDATE ledger_snapshots
|
|
SET status = @expiredStatus, updated_at = @updatedAt
|
|
WHERE expires_at IS NOT NULL
|
|
AND expires_at < @cutoff
|
|
AND status = @availableStatus
|
|
""";
|
|
|
|
await using var cmd = _dataSource.CreateCommand(sql);
|
|
cmd.Parameters.AddWithValue("expiredStatus", SnapshotStatus.Expired.ToString());
|
|
cmd.Parameters.AddWithValue("availableStatus", SnapshotStatus.Available.ToString());
|
|
cmd.Parameters.AddWithValue("cutoff", cutoff);
|
|
cmd.Parameters.AddWithValue("updatedAt", DateTimeOffset.UtcNow);
|
|
|
|
return await cmd.ExecuteNonQueryAsync(ct);
|
|
}
|
|
|
|
public async Task<bool> DeleteAsync(
|
|
string tenantId,
|
|
Guid snapshotId,
|
|
CancellationToken ct = default)
|
|
{
|
|
const string sql = """
|
|
UPDATE ledger_snapshots
|
|
SET status = @deletedStatus, updated_at = @updatedAt
|
|
WHERE tenant_id = @tenantId AND snapshot_id = @snapshotId
|
|
""";
|
|
|
|
await using var cmd = _dataSource.CreateCommand(sql);
|
|
cmd.Parameters.AddWithValue("tenantId", tenantId);
|
|
cmd.Parameters.AddWithValue("snapshotId", snapshotId);
|
|
cmd.Parameters.AddWithValue("deletedStatus", SnapshotStatus.Deleted.ToString());
|
|
cmd.Parameters.AddWithValue("updatedAt", DateTimeOffset.UtcNow);
|
|
|
|
return await cmd.ExecuteNonQueryAsync(ct) > 0;
|
|
}
|
|
|
|
public async Task<LedgerSnapshot?> GetLatestAsync(
|
|
string tenantId,
|
|
CancellationToken ct = default)
|
|
{
|
|
const string sql = """
|
|
SELECT tenant_id, snapshot_id, label, description, status,
|
|
created_at, expires_at, sequence_number, snapshot_timestamp,
|
|
findings_count, vex_statements_count, advisories_count,
|
|
sboms_count, events_count, size_bytes,
|
|
merkle_root, dsse_digest, metadata
|
|
FROM ledger_snapshots
|
|
WHERE tenant_id = @tenantId AND status = @status
|
|
ORDER BY created_at DESC
|
|
LIMIT 1
|
|
""";
|
|
|
|
await using var cmd = _dataSource.CreateCommand(sql);
|
|
cmd.Parameters.AddWithValue("tenantId", tenantId);
|
|
cmd.Parameters.AddWithValue("status", SnapshotStatus.Available.ToString());
|
|
|
|
await using var reader = await cmd.ExecuteReaderAsync(ct);
|
|
if (!await reader.ReadAsync(ct))
|
|
return null;
|
|
|
|
return MapSnapshot(reader);
|
|
}
|
|
|
|
public async Task<bool> ExistsAsync(
|
|
string tenantId,
|
|
Guid snapshotId,
|
|
CancellationToken ct = default)
|
|
{
|
|
const string sql = """
|
|
SELECT 1 FROM ledger_snapshots
|
|
WHERE tenant_id = @tenantId AND snapshot_id = @snapshotId
|
|
LIMIT 1
|
|
""";
|
|
|
|
await using var cmd = _dataSource.CreateCommand(sql);
|
|
cmd.Parameters.AddWithValue("tenantId", tenantId);
|
|
cmd.Parameters.AddWithValue("snapshotId", snapshotId);
|
|
|
|
await using var reader = await cmd.ExecuteReaderAsync(ct);
|
|
return await reader.ReadAsync(ct);
|
|
}
|
|
|
|
private LedgerSnapshot MapSnapshot(NpgsqlDataReader reader)
|
|
{
|
|
var metadataJson = reader.IsDBNull(reader.GetOrdinal("metadata"))
|
|
? null
|
|
: reader.GetString(reader.GetOrdinal("metadata"));
|
|
|
|
Dictionary<string, object>? metadata = null;
|
|
if (!string.IsNullOrEmpty(metadataJson))
|
|
{
|
|
metadata = JsonSerializer.Deserialize<Dictionary<string, object>>(metadataJson, _jsonOptions);
|
|
}
|
|
|
|
return new LedgerSnapshot(
|
|
TenantId: reader.GetString(reader.GetOrdinal("tenant_id")),
|
|
SnapshotId: reader.GetGuid(reader.GetOrdinal("snapshot_id")),
|
|
Label: reader.IsDBNull(reader.GetOrdinal("label")) ? null : reader.GetString(reader.GetOrdinal("label")),
|
|
Description: reader.IsDBNull(reader.GetOrdinal("description")) ? null : reader.GetString(reader.GetOrdinal("description")),
|
|
Status: Enum.Parse<SnapshotStatus>(reader.GetString(reader.GetOrdinal("status"))),
|
|
CreatedAt: reader.GetFieldValue<DateTimeOffset>(reader.GetOrdinal("created_at")),
|
|
ExpiresAt: reader.IsDBNull(reader.GetOrdinal("expires_at")) ? null : reader.GetFieldValue<DateTimeOffset>(reader.GetOrdinal("expires_at")),
|
|
SequenceNumber: reader.GetInt64(reader.GetOrdinal("sequence_number")),
|
|
Timestamp: reader.GetFieldValue<DateTimeOffset>(reader.GetOrdinal("snapshot_timestamp")),
|
|
Statistics: new SnapshotStatistics(
|
|
FindingsCount: reader.GetInt64(reader.GetOrdinal("findings_count")),
|
|
VexStatementsCount: reader.GetInt64(reader.GetOrdinal("vex_statements_count")),
|
|
AdvisoriesCount: reader.GetInt64(reader.GetOrdinal("advisories_count")),
|
|
SbomsCount: reader.GetInt64(reader.GetOrdinal("sboms_count")),
|
|
EventsCount: reader.GetInt64(reader.GetOrdinal("events_count")),
|
|
SizeBytes: reader.GetInt64(reader.GetOrdinal("size_bytes"))),
|
|
MerkleRoot: reader.IsDBNull(reader.GetOrdinal("merkle_root")) ? null : reader.GetString(reader.GetOrdinal("merkle_root")),
|
|
DsseDigest: reader.IsDBNull(reader.GetOrdinal("dsse_digest")) ? null : reader.GetString(reader.GetOrdinal("dsse_digest")),
|
|
Metadata: metadata);
|
|
}
|
|
}
|