Add unit tests for SBOM ingestion and transformation
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Implement `SbomIngestServiceCollectionExtensionsTests` to verify the SBOM ingestion pipeline exports snapshots correctly. - Create `SbomIngestTransformerTests` to ensure the transformation produces expected nodes and edges, including deduplication of license nodes and normalization of timestamps. - Add `SbomSnapshotExporterTests` to test the export functionality for manifest, adjacency, nodes, and edges. - Introduce `VexOverlayTransformerTests` to validate the transformation of VEX nodes and edges. - Set up project file for the test project with necessary dependencies and configurations. - Include JSON fixture files for testing purposes.
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using NpgsqlTypes;
|
||||
using StellaOps.Findings.Ledger.Domain;
|
||||
using StellaOps.Findings.Ledger.Hashing;
|
||||
|
||||
namespace StellaOps.Findings.Ledger.Infrastructure.Postgres;
|
||||
|
||||
public sealed class PostgresLedgerEventRepository : ILedgerEventRepository
|
||||
{
|
||||
private const string SelectByEventIdSql = """
|
||||
SELECT chain_id,
|
||||
sequence_no,
|
||||
event_type,
|
||||
policy_version,
|
||||
finding_id,
|
||||
artifact_id,
|
||||
source_run_id,
|
||||
actor_id,
|
||||
actor_type,
|
||||
occurred_at,
|
||||
recorded_at,
|
||||
event_body,
|
||||
event_hash,
|
||||
previous_hash,
|
||||
merkle_leaf_hash
|
||||
FROM ledger_events
|
||||
WHERE tenant_id = @tenant_id
|
||||
AND event_id = @event_id
|
||||
""";
|
||||
|
||||
private const string SelectChainHeadSql = """
|
||||
SELECT sequence_no,
|
||||
event_hash,
|
||||
recorded_at
|
||||
FROM ledger_events
|
||||
WHERE tenant_id = @tenant_id
|
||||
AND chain_id = @chain_id
|
||||
ORDER BY sequence_no DESC
|
||||
LIMIT 1
|
||||
""";
|
||||
|
||||
private const string InsertEventSql = """
|
||||
INSERT INTO ledger_events (
|
||||
tenant_id,
|
||||
chain_id,
|
||||
sequence_no,
|
||||
event_id,
|
||||
event_type,
|
||||
policy_version,
|
||||
finding_id,
|
||||
artifact_id,
|
||||
source_run_id,
|
||||
actor_id,
|
||||
actor_type,
|
||||
occurred_at,
|
||||
recorded_at,
|
||||
event_body,
|
||||
event_hash,
|
||||
previous_hash,
|
||||
merkle_leaf_hash)
|
||||
VALUES (
|
||||
@tenant_id,
|
||||
@chain_id,
|
||||
@sequence_no,
|
||||
@event_id,
|
||||
@event_type,
|
||||
@policy_version,
|
||||
@finding_id,
|
||||
@artifact_id,
|
||||
@source_run_id,
|
||||
@actor_id,
|
||||
@actor_type,
|
||||
@occurred_at,
|
||||
@recorded_at,
|
||||
@event_body,
|
||||
@event_hash,
|
||||
@previous_hash,
|
||||
@merkle_leaf_hash)
|
||||
""";
|
||||
|
||||
private readonly LedgerDataSource _dataSource;
|
||||
private readonly ILogger<PostgresLedgerEventRepository> _logger;
|
||||
|
||||
public PostgresLedgerEventRepository(
|
||||
LedgerDataSource dataSource,
|
||||
ILogger<PostgresLedgerEventRepository> logger)
|
||||
{
|
||||
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task<LedgerEventRecord?> GetByEventIdAsync(string tenantId, Guid eventId, CancellationToken cancellationToken)
|
||||
{
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(tenantId, cancellationToken).ConfigureAwait(false);
|
||||
await using var command = new NpgsqlCommand(SelectByEventIdSql, connection);
|
||||
command.CommandTimeout = _dataSource.CommandTimeoutSeconds;
|
||||
command.Parameters.AddWithValue("tenant_id", tenantId);
|
||||
command.Parameters.AddWithValue("event_id", eventId);
|
||||
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return MapLedgerEventRecord(tenantId, eventId, reader);
|
||||
}
|
||||
|
||||
public async Task<LedgerChainHead?> GetChainHeadAsync(string tenantId, Guid chainId, CancellationToken cancellationToken)
|
||||
{
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(tenantId, cancellationToken).ConfigureAwait(false);
|
||||
await using var command = new NpgsqlCommand(SelectChainHeadSql, connection);
|
||||
command.CommandTimeout = _dataSource.CommandTimeoutSeconds;
|
||||
command.Parameters.AddWithValue("tenant_id", tenantId);
|
||||
command.Parameters.AddWithValue("chain_id", chainId);
|
||||
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var sequenceNumber = reader.GetInt64(0);
|
||||
var eventHash = reader.GetString(1);
|
||||
var recordedAt = reader.GetFieldValue<DateTimeOffset>(2);
|
||||
return new LedgerChainHead(sequenceNumber, eventHash, recordedAt);
|
||||
}
|
||||
|
||||
public async Task AppendAsync(LedgerEventRecord record, CancellationToken cancellationToken)
|
||||
{
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(record.TenantId, cancellationToken).ConfigureAwait(false);
|
||||
await using var command = new NpgsqlCommand(InsertEventSql, connection);
|
||||
command.CommandTimeout = _dataSource.CommandTimeoutSeconds;
|
||||
|
||||
command.Parameters.AddWithValue("tenant_id", record.TenantId);
|
||||
command.Parameters.AddWithValue("chain_id", record.ChainId);
|
||||
command.Parameters.AddWithValue("sequence_no", record.SequenceNumber);
|
||||
command.Parameters.AddWithValue("event_id", record.EventId);
|
||||
command.Parameters.AddWithValue("event_type", record.EventType);
|
||||
command.Parameters.AddWithValue("policy_version", record.PolicyVersion);
|
||||
command.Parameters.AddWithValue("finding_id", record.FindingId);
|
||||
command.Parameters.AddWithValue("artifact_id", record.ArtifactId);
|
||||
|
||||
if (record.SourceRunId.HasValue)
|
||||
{
|
||||
command.Parameters.AddWithValue("source_run_id", record.SourceRunId.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
command.Parameters.AddWithValue("source_run_id", DBNull.Value);
|
||||
}
|
||||
|
||||
command.Parameters.AddWithValue("actor_id", record.ActorId);
|
||||
command.Parameters.AddWithValue("actor_type", record.ActorType);
|
||||
command.Parameters.AddWithValue("occurred_at", record.OccurredAt);
|
||||
command.Parameters.AddWithValue("recorded_at", record.RecordedAt);
|
||||
|
||||
var eventBody = record.EventBody.ToJsonString();
|
||||
command.Parameters.Add(new NpgsqlParameter<string>("event_body", NpgsqlDbType.Jsonb) { TypedValue = eventBody });
|
||||
command.Parameters.AddWithValue("event_hash", record.EventHash);
|
||||
command.Parameters.AddWithValue("previous_hash", record.PreviousHash);
|
||||
command.Parameters.AddWithValue("merkle_leaf_hash", record.MerkleLeafHash);
|
||||
|
||||
try
|
||||
{
|
||||
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (PostgresException ex) when (string.Equals(ex.SqlState, PostgresErrorCodes.UniqueViolation, StringComparison.Ordinal))
|
||||
{
|
||||
throw new LedgerDuplicateEventException(record.EventId, ex);
|
||||
}
|
||||
}
|
||||
|
||||
internal static LedgerEventRecord MapLedgerEventRecord(string tenantId, Guid eventId, NpgsqlDataReader reader)
|
||||
{
|
||||
var chainId = reader.GetFieldValue<Guid>(0);
|
||||
var sequenceNumber = reader.GetInt64(1);
|
||||
var eventType = reader.GetString(2);
|
||||
var policyVersion = reader.GetString(3);
|
||||
var findingId = reader.GetString(4);
|
||||
var artifactId = reader.GetString(5);
|
||||
var sourceRunId = reader.IsDBNull(6) ? (Guid?)null : reader.GetGuid(6);
|
||||
var actorId = reader.GetString(7);
|
||||
var actorType = reader.GetString(8);
|
||||
var occurredAt = reader.GetFieldValue<DateTimeOffset>(9);
|
||||
var recordedAt = reader.GetFieldValue<DateTimeOffset>(10);
|
||||
|
||||
var eventBodyJson = reader.GetFieldValue<string>(11);
|
||||
var eventBody = JsonNode.Parse(eventBodyJson)?.AsObject()
|
||||
?? throw new InvalidOperationException("Failed to parse ledger event body.");
|
||||
|
||||
var eventHash = reader.GetString(12);
|
||||
var previousHash = reader.GetString(13);
|
||||
var merkleLeafHash = reader.GetString(14);
|
||||
|
||||
var canonicalEnvelope = LedgerCanonicalJsonSerializer.Canonicalize(eventBody);
|
||||
var canonicalJson = LedgerCanonicalJsonSerializer.Serialize(canonicalEnvelope);
|
||||
|
||||
return new LedgerEventRecord(
|
||||
tenantId,
|
||||
chainId,
|
||||
sequenceNumber,
|
||||
eventId,
|
||||
eventType,
|
||||
policyVersion,
|
||||
findingId,
|
||||
artifactId,
|
||||
sourceRunId,
|
||||
actorId,
|
||||
actorType,
|
||||
occurredAt,
|
||||
recordedAt,
|
||||
eventBody,
|
||||
eventHash,
|
||||
previousHash,
|
||||
merkleLeafHash,
|
||||
canonicalJson);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user