update evidence bundle to include new evidence types and implement ProofSpine integration
Some checks failed
Lighthouse CI / Lighthouse Audit (push) Has been cancelled
Lighthouse CI / Axe Accessibility Audit (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
sm-remote-ci / build-and-test (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Lighthouse CI / Lighthouse Audit (push) Has been cancelled
Lighthouse CI / Axe Accessibility Audit (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
sm-remote-ci / build-and-test (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
This commit is contained in:
@@ -11,6 +11,7 @@ using Microsoft.Extensions.Options;
|
||||
using StellaOps.Infrastructure.Postgres.Migrations;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
using StellaOps.Scanner.EntryTrace;
|
||||
using StellaOps.Scanner.ProofSpine;
|
||||
using StellaOps.Scanner.Storage.ObjectStore;
|
||||
using StellaOps.Scanner.Storage.Postgres;
|
||||
using StellaOps.Scanner.Storage.Repositories;
|
||||
@@ -73,6 +74,7 @@ public static class ServiceCollectionExtensions
|
||||
services.AddScoped<EntryTraceRepository>();
|
||||
services.AddScoped<RubyPackageInventoryRepository>();
|
||||
services.AddScoped<BunPackageInventoryRepository>();
|
||||
services.AddScoped<IProofSpineRepository, PostgresProofSpineRepository>();
|
||||
services.AddSingleton<IEntryTraceResultStore, EntryTraceResultStore>();
|
||||
services.AddSingleton<IRubyPackageInventoryStore, RubyPackageInventoryStore>();
|
||||
services.AddSingleton<IBunPackageInventoryStore, BunPackageInventoryStore>();
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
-- proof spine storage schema (startup migration)
|
||||
-- schema: created externally via search_path; tables unqualified for scanner schema compatibility
|
||||
|
||||
CREATE TABLE IF NOT EXISTS proof_spines (
|
||||
spine_id TEXT PRIMARY KEY,
|
||||
artifact_id TEXT NOT NULL,
|
||||
vuln_id TEXT NOT NULL,
|
||||
policy_profile_id TEXT NOT NULL,
|
||||
verdict TEXT NOT NULL,
|
||||
verdict_reason TEXT,
|
||||
root_hash TEXT NOT NULL,
|
||||
scan_run_id TEXT NOT NULL,
|
||||
segment_count INT NOT NULL,
|
||||
created_at_utc TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
superseded_by_spine_id TEXT REFERENCES proof_spines(spine_id),
|
||||
|
||||
CONSTRAINT proof_spines_unique_decision UNIQUE (artifact_id, vuln_id, policy_profile_id, root_hash)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_proof_spines_lookup
|
||||
ON proof_spines(artifact_id, vuln_id, policy_profile_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_proof_spines_scan_run
|
||||
ON proof_spines(scan_run_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_proof_spines_created_at
|
||||
ON proof_spines(created_at_utc DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS proof_segments (
|
||||
segment_id TEXT PRIMARY KEY,
|
||||
spine_id TEXT NOT NULL REFERENCES proof_spines(spine_id) ON DELETE CASCADE,
|
||||
idx INT NOT NULL,
|
||||
segment_type TEXT NOT NULL,
|
||||
input_hash TEXT NOT NULL,
|
||||
result_hash TEXT NOT NULL,
|
||||
prev_segment_hash TEXT,
|
||||
envelope_json TEXT NOT NULL,
|
||||
tool_id TEXT NOT NULL,
|
||||
tool_version TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
created_at_utc TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT proof_segments_unique_index UNIQUE (spine_id, idx)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_proof_segments_spine_idx
|
||||
ON proof_segments(spine_id, idx);
|
||||
CREATE INDEX IF NOT EXISTS ix_proof_segments_type
|
||||
ON proof_segments(segment_type);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS proof_spine_history (
|
||||
id TEXT PRIMARY KEY,
|
||||
old_spine_id TEXT NOT NULL REFERENCES proof_spines(spine_id) ON DELETE CASCADE,
|
||||
new_spine_id TEXT NOT NULL REFERENCES proof_spines(spine_id) ON DELETE CASCADE,
|
||||
reason TEXT NOT NULL,
|
||||
created_at_utc TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_proof_spine_history_old
|
||||
ON proof_spine_history(old_spine_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_proof_spine_history_new
|
||||
ON proof_spine_history(new_spine_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_proof_spine_history_created_at
|
||||
ON proof_spine_history(created_at_utc DESC);
|
||||
|
||||
@@ -3,4 +3,5 @@ namespace StellaOps.Scanner.Storage.Postgres.Migrations;
|
||||
internal static class MigrationIds
|
||||
{
|
||||
public const string CreateTables = "001_create_tables.sql";
|
||||
public const string ProofSpineTables = "002_proof_spine_tables.sql";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,397 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
using StellaOps.Replay.Core;
|
||||
using StellaOps.Scanner.ProofSpine;
|
||||
using StellaOps.Scanner.Storage.Postgres;
|
||||
using ProofSpineModel = StellaOps.Scanner.ProofSpine.ProofSpine;
|
||||
|
||||
namespace StellaOps.Scanner.Storage.Repositories;
|
||||
|
||||
public sealed class PostgresProofSpineRepository : RepositoryBase<ScannerDataSource>, IProofSpineRepository
|
||||
{
|
||||
private const string Tenant = "";
|
||||
|
||||
private string SchemaName => DataSource.SchemaName ?? ScannerDataSource.DefaultSchema;
|
||||
|
||||
private string SpinesTable => $"{SchemaName}.proof_spines";
|
||||
|
||||
private string SegmentsTable => $"{SchemaName}.proof_segments";
|
||||
|
||||
private string HistoryTable => $"{SchemaName}.proof_spine_history";
|
||||
|
||||
private static readonly JsonSerializerOptions LenientJson = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public PostgresProofSpineRepository(
|
||||
ScannerDataSource dataSource,
|
||||
ILogger<PostgresProofSpineRepository> logger,
|
||||
TimeProvider? timeProvider = null)
|
||||
: base(dataSource, logger)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public Task<ProofSpineModel?> GetByIdAsync(string spineId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(spineId);
|
||||
|
||||
var sql = $"""
|
||||
SELECT spine_id, artifact_id, vuln_id, policy_profile_id,
|
||||
verdict, verdict_reason, root_hash, scan_run_id,
|
||||
created_at_utc, superseded_by_spine_id
|
||||
FROM {SpinesTable}
|
||||
WHERE spine_id = @spine_id
|
||||
""";
|
||||
|
||||
return QuerySingleOrDefaultAsync(
|
||||
Tenant,
|
||||
sql,
|
||||
cmd => AddParameter(cmd, "spine_id", spineId.Trim()),
|
||||
MapSpine,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public Task<ProofSpineModel?> GetByDecisionAsync(
|
||||
string artifactId,
|
||||
string vulnId,
|
||||
string policyProfileId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(artifactId);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(vulnId);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(policyProfileId);
|
||||
|
||||
var sql = $"""
|
||||
SELECT spine_id, artifact_id, vuln_id, policy_profile_id,
|
||||
verdict, verdict_reason, root_hash, scan_run_id,
|
||||
created_at_utc, superseded_by_spine_id
|
||||
FROM {SpinesTable}
|
||||
WHERE artifact_id = @artifact_id
|
||||
AND vuln_id = @vuln_id
|
||||
AND policy_profile_id = @policy_profile_id
|
||||
ORDER BY created_at_utc DESC, spine_id DESC
|
||||
LIMIT 1
|
||||
""";
|
||||
|
||||
return QuerySingleOrDefaultAsync(
|
||||
Tenant,
|
||||
sql,
|
||||
cmd =>
|
||||
{
|
||||
AddParameter(cmd, "artifact_id", artifactId.Trim());
|
||||
AddParameter(cmd, "vuln_id", vulnId.Trim());
|
||||
AddParameter(cmd, "policy_profile_id", policyProfileId.Trim());
|
||||
},
|
||||
MapSpine,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<ProofSpineModel>> GetByScanRunAsync(
|
||||
string scanRunId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(scanRunId);
|
||||
|
||||
var sql = $"""
|
||||
SELECT spine_id, artifact_id, vuln_id, policy_profile_id,
|
||||
verdict, verdict_reason, root_hash, scan_run_id,
|
||||
created_at_utc, superseded_by_spine_id
|
||||
FROM {SpinesTable}
|
||||
WHERE scan_run_id = @scan_run_id
|
||||
ORDER BY created_at_utc DESC, spine_id DESC
|
||||
""";
|
||||
|
||||
return QueryAsync(
|
||||
Tenant,
|
||||
sql,
|
||||
cmd => AddParameter(cmd, "scan_run_id", scanRunId.Trim()),
|
||||
MapSpine,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<ProofSpineModel> SaveAsync(ProofSpineModel spine, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(spine);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (spine.Segments is null || spine.Segments.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("ProofSpine requires at least one segment.");
|
||||
}
|
||||
|
||||
var createdAt = spine.CreatedAt == default ? _timeProvider.GetUtcNow() : spine.CreatedAt;
|
||||
|
||||
await using var connection = await DataSource.OpenConnectionAsync(Tenant, "writer", cancellationToken).ConfigureAwait(false);
|
||||
await using var transaction = await connection.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
var insertSpine = $"""
|
||||
INSERT INTO {SpinesTable} (
|
||||
spine_id, artifact_id, vuln_id, policy_profile_id,
|
||||
verdict, verdict_reason, root_hash, scan_run_id,
|
||||
segment_count, created_at_utc, superseded_by_spine_id
|
||||
)
|
||||
VALUES (
|
||||
@spine_id, @artifact_id, @vuln_id, @policy_profile_id,
|
||||
@verdict, @verdict_reason, @root_hash, @scan_run_id,
|
||||
@segment_count, @created_at_utc, @superseded_by_spine_id
|
||||
)
|
||||
ON CONFLICT (spine_id) DO NOTHING
|
||||
""";
|
||||
|
||||
await using (var command = CreateCommand(insertSpine, connection))
|
||||
{
|
||||
command.Transaction = transaction;
|
||||
AddParameter(command, "spine_id", spine.SpineId);
|
||||
AddParameter(command, "artifact_id", spine.ArtifactId);
|
||||
AddParameter(command, "vuln_id", spine.VulnerabilityId);
|
||||
AddParameter(command, "policy_profile_id", spine.PolicyProfileId);
|
||||
AddParameter(command, "verdict", spine.Verdict);
|
||||
AddParameter(command, "verdict_reason", spine.VerdictReason);
|
||||
AddParameter(command, "root_hash", spine.RootHash);
|
||||
AddParameter(command, "scan_run_id", spine.ScanRunId);
|
||||
AddParameter(command, "segment_count", spine.Segments.Count);
|
||||
AddParameter(command, "created_at_utc", createdAt);
|
||||
AddParameter(command, "superseded_by_spine_id", spine.SupersededBySpineId);
|
||||
|
||||
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var insertSegment = $"""
|
||||
INSERT INTO {SegmentsTable} (
|
||||
segment_id, spine_id, idx, segment_type, input_hash, result_hash, prev_segment_hash,
|
||||
envelope_json, tool_id, tool_version, status, created_at_utc
|
||||
)
|
||||
VALUES (
|
||||
@segment_id, @spine_id, @idx, @segment_type, @input_hash, @result_hash, @prev_segment_hash,
|
||||
@envelope_json, @tool_id, @tool_version, @status, @created_at_utc
|
||||
)
|
||||
ON CONFLICT (segment_id) DO NOTHING
|
||||
""";
|
||||
|
||||
foreach (var segment in spine.Segments.OrderBy(s => s.Index))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
await using var command = CreateCommand(insertSegment, connection);
|
||||
command.Transaction = transaction;
|
||||
|
||||
AddParameter(command, "segment_id", segment.SegmentId);
|
||||
AddParameter(command, "spine_id", spine.SpineId);
|
||||
AddParameter(command, "idx", segment.Index);
|
||||
AddParameter(command, "segment_type", segment.SegmentType.ToString());
|
||||
AddParameter(command, "input_hash", segment.InputHash);
|
||||
AddParameter(command, "result_hash", segment.ResultHash);
|
||||
AddParameter(command, "prev_segment_hash", segment.PrevSegmentHash);
|
||||
AddParameter(command, "envelope_json", SerializeEnvelope(segment.Envelope));
|
||||
AddParameter(command, "tool_id", segment.ToolId);
|
||||
AddParameter(command, "tool_version", segment.ToolVersion);
|
||||
AddParameter(command, "status", segment.Status.ToString());
|
||||
AddParameter(command, "created_at_utc", segment.CreatedAt == default ? createdAt : segment.CreatedAt);
|
||||
|
||||
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await transaction.RollbackAsync(cancellationToken).ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
|
||||
return spine with { CreatedAt = createdAt };
|
||||
}
|
||||
|
||||
public async Task SupersedeAsync(
|
||||
string oldSpineId,
|
||||
string newSpineId,
|
||||
string reason,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(oldSpineId);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(newSpineId);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(reason);
|
||||
|
||||
await using var connection = await DataSource.OpenConnectionAsync(Tenant, "writer", cancellationToken).ConfigureAwait(false);
|
||||
await using var transaction = await connection.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
var update = $"""
|
||||
UPDATE {SpinesTable}
|
||||
SET superseded_by_spine_id = @new_spine_id
|
||||
WHERE spine_id = @old_spine_id
|
||||
""";
|
||||
|
||||
await using (var command = CreateCommand(update, connection))
|
||||
{
|
||||
command.Transaction = transaction;
|
||||
AddParameter(command, "old_spine_id", oldSpineId.Trim());
|
||||
AddParameter(command, "new_spine_id", newSpineId.Trim());
|
||||
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var insertHistory = $"""
|
||||
INSERT INTO {HistoryTable} (id, old_spine_id, new_spine_id, reason, created_at_utc)
|
||||
VALUES (@id, @old_spine_id, @new_spine_id, @reason, @created_at_utc)
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
""";
|
||||
|
||||
await using (var command = CreateCommand(insertHistory, connection))
|
||||
{
|
||||
command.Transaction = transaction;
|
||||
AddParameter(command, "id", Guid.NewGuid().ToString("N"));
|
||||
AddParameter(command, "old_spine_id", oldSpineId.Trim());
|
||||
AddParameter(command, "new_spine_id", newSpineId.Trim());
|
||||
AddParameter(command, "reason", reason);
|
||||
AddParameter(command, "created_at_utc", _timeProvider.GetUtcNow());
|
||||
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await transaction.RollbackAsync(cancellationToken).ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<ProofSegment>> GetSegmentsAsync(string spineId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(spineId);
|
||||
|
||||
var sql = $"""
|
||||
SELECT segment_id, segment_type, idx, input_hash, result_hash, prev_segment_hash,
|
||||
envelope_json, tool_id, tool_version, status, created_at_utc
|
||||
FROM {SegmentsTable}
|
||||
WHERE spine_id = @spine_id
|
||||
ORDER BY idx
|
||||
""";
|
||||
|
||||
return QueryAsync(
|
||||
Tenant,
|
||||
sql,
|
||||
cmd => AddParameter(cmd, "spine_id", spineId.Trim()),
|
||||
MapSegment,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<ProofSpineSummary>> GetSummariesByScanRunAsync(
|
||||
string scanRunId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(scanRunId);
|
||||
|
||||
var sql = $"""
|
||||
SELECT spine_id, artifact_id, vuln_id, verdict, segment_count, created_at_utc
|
||||
FROM {SpinesTable}
|
||||
WHERE scan_run_id = @scan_run_id
|
||||
ORDER BY created_at_utc DESC, spine_id DESC
|
||||
""";
|
||||
|
||||
return QueryAsync(
|
||||
Tenant,
|
||||
sql,
|
||||
cmd => AddParameter(cmd, "scan_run_id", scanRunId.Trim()),
|
||||
MapSummary,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private static ProofSpineModel MapSpine(NpgsqlDataReader reader)
|
||||
{
|
||||
return new ProofSpineModel(
|
||||
SpineId: reader.GetString(reader.GetOrdinal("spine_id")),
|
||||
ArtifactId: reader.GetString(reader.GetOrdinal("artifact_id")),
|
||||
VulnerabilityId: reader.GetString(reader.GetOrdinal("vuln_id")),
|
||||
PolicyProfileId: reader.GetString(reader.GetOrdinal("policy_profile_id")),
|
||||
Segments: Array.Empty<ProofSegment>(),
|
||||
Verdict: reader.GetString(reader.GetOrdinal("verdict")),
|
||||
VerdictReason: reader.IsDBNull(reader.GetOrdinal("verdict_reason"))
|
||||
? string.Empty
|
||||
: reader.GetString(reader.GetOrdinal("verdict_reason")),
|
||||
RootHash: reader.GetString(reader.GetOrdinal("root_hash")),
|
||||
ScanRunId: reader.GetString(reader.GetOrdinal("scan_run_id")),
|
||||
CreatedAt: reader.GetFieldValue<DateTimeOffset>(reader.GetOrdinal("created_at_utc")),
|
||||
SupersededBySpineId: GetNullableString(reader, reader.GetOrdinal("superseded_by_spine_id")));
|
||||
}
|
||||
|
||||
private static ProofSegment MapSegment(NpgsqlDataReader reader)
|
||||
{
|
||||
var segmentTypeString = reader.GetString(reader.GetOrdinal("segment_type"));
|
||||
if (!Enum.TryParse<ProofSegmentType>(segmentTypeString, ignoreCase: true, out var segmentType))
|
||||
{
|
||||
throw new InvalidOperationException($"Unsupported proof segment type '{segmentTypeString}'.");
|
||||
}
|
||||
|
||||
var statusString = reader.GetString(reader.GetOrdinal("status"));
|
||||
if (!Enum.TryParse<ProofSegmentStatus>(statusString, ignoreCase: true, out var status))
|
||||
{
|
||||
status = ProofSegmentStatus.Pending;
|
||||
}
|
||||
|
||||
var envelopeJson = reader.GetString(reader.GetOrdinal("envelope_json"));
|
||||
|
||||
return new ProofSegment(
|
||||
SegmentId: reader.GetString(reader.GetOrdinal("segment_id")),
|
||||
SegmentType: segmentType,
|
||||
Index: reader.GetInt32(reader.GetOrdinal("idx")),
|
||||
InputHash: reader.GetString(reader.GetOrdinal("input_hash")),
|
||||
ResultHash: reader.GetString(reader.GetOrdinal("result_hash")),
|
||||
PrevSegmentHash: GetNullableString(reader, reader.GetOrdinal("prev_segment_hash")),
|
||||
Envelope: DeserializeEnvelope(envelopeJson),
|
||||
ToolId: reader.GetString(reader.GetOrdinal("tool_id")),
|
||||
ToolVersion: reader.GetString(reader.GetOrdinal("tool_version")),
|
||||
Status: status,
|
||||
CreatedAt: reader.GetFieldValue<DateTimeOffset>(reader.GetOrdinal("created_at_utc")));
|
||||
}
|
||||
|
||||
private static string SerializeEnvelope(DsseEnvelope envelope)
|
||||
{
|
||||
var doc = new DsseEnvelopeDocument(
|
||||
envelope.PayloadType,
|
||||
envelope.Payload,
|
||||
envelope.Signatures.Select(s => new DsseSignatureDocument(s.KeyId, s.Sig)).ToArray());
|
||||
|
||||
return CanonicalJson.Serialize(doc);
|
||||
}
|
||||
|
||||
private static DsseEnvelope DeserializeEnvelope(string json)
|
||||
{
|
||||
var doc = JsonSerializer.Deserialize<DsseEnvelopeDocument>(json, LenientJson)
|
||||
?? throw new InvalidOperationException("DSSE envelope deserialized to null.");
|
||||
|
||||
var signatures = doc.Signatures is null
|
||||
? Array.Empty<DsseSignature>()
|
||||
: doc.Signatures.Select(s => new DsseSignature(s.KeyId, s.Sig)).ToArray();
|
||||
|
||||
return new DsseEnvelope(doc.PayloadType, doc.Payload, signatures);
|
||||
}
|
||||
|
||||
private sealed record DsseEnvelopeDocument(
|
||||
[property: JsonPropertyName("payloadType")] string PayloadType,
|
||||
[property: JsonPropertyName("payload")] string Payload,
|
||||
[property: JsonPropertyName("signatures")] IReadOnlyList<DsseSignatureDocument> Signatures);
|
||||
|
||||
private sealed record DsseSignatureDocument(
|
||||
[property: JsonPropertyName("keyid")] string KeyId,
|
||||
[property: JsonPropertyName("sig")] string Sig);
|
||||
|
||||
private static ProofSpineSummary MapSummary(NpgsqlDataReader reader)
|
||||
=> new(
|
||||
SpineId: reader.GetString(reader.GetOrdinal("spine_id")),
|
||||
ArtifactId: reader.GetString(reader.GetOrdinal("artifact_id")),
|
||||
VulnerabilityId: reader.GetString(reader.GetOrdinal("vuln_id")),
|
||||
Verdict: reader.GetString(reader.GetOrdinal("verdict")),
|
||||
SegmentCount: reader.GetInt32(reader.GetOrdinal("segment_count")),
|
||||
CreatedAt: reader.GetFieldValue<DateTimeOffset>(reader.GetOrdinal("created_at_utc")));
|
||||
}
|
||||
@@ -21,6 +21,7 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\\StellaOps.Scanner.EntryTrace\\StellaOps.Scanner.EntryTrace.csproj" />
|
||||
<ProjectReference Include="..\\StellaOps.Scanner.Core\\StellaOps.Scanner.Core.csproj" />
|
||||
<ProjectReference Include="..\\StellaOps.Scanner.ProofSpine\\StellaOps.Scanner.ProofSpine.csproj" />
|
||||
<ProjectReference Include="..\\..\\..\\__Libraries\\StellaOps.Infrastructure.Postgres\\StellaOps.Infrastructure.Postgres.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user