doctor enhancements, setup, enhancements, ui functionality and design consolidation and , test projects fixes , product advisory attestation/rekor and delta verfications enhancements

This commit is contained in:
master
2026-01-19 09:02:59 +02:00
parent 8c4bf54aed
commit 17419ba7c4
809 changed files with 170738 additions and 12244 deletions

View File

@@ -0,0 +1,75 @@
// -----------------------------------------------------------------------------
// VerdictLedgerEntry.cs
// Sprint: SPRINT_20260118_015_Attestor_verdict_ledger_foundation
// Task: VL-002 - Implement VerdictLedger entity and repository
// Description: Domain entity for append-only verdict ledger
// -----------------------------------------------------------------------------
namespace StellaOps.Attestor.Persistence.Entities;
/// <summary>
/// Represents an entry in the append-only verdict ledger.
/// Each entry is cryptographically chained to the previous entry via SHA-256 hashes.
/// </summary>
public sealed record VerdictLedgerEntry
{
/// <summary>Primary identifier.</summary>
public Guid LedgerId { get; init; } = Guid.NewGuid();
/// <summary>Package URL or container digest reference.</summary>
public required string BomRef { get; init; }
/// <summary>CycloneDX serialNumber URN (urn:uuid:...).</summary>
public string? CycloneDxSerial { get; init; }
/// <summary>Rekor transparency log entry UUID (populated after submission).</summary>
public string? RekorUuid { get; init; }
/// <summary>Verdict decision.</summary>
public VerdictDecision Decision { get; init; } = VerdictDecision.Unknown;
/// <summary>Human-readable reason for this verdict.</summary>
public string? Reason { get; init; }
/// <summary>Policy bundle identifier used for this decision.</summary>
public required string PolicyBundleId { get; init; }
/// <summary>SHA-256 hash of policy bundle content.</summary>
public required string PolicyBundleHash { get; init; }
/// <summary>Container digest of the verifier service that made this decision.</summary>
public required string VerifierImageDigest { get; init; }
/// <summary>Key ID that signed this verdict.</summary>
public required string SignerKeyId { get; init; }
/// <summary>SHA-256 hash of the previous entry (null for genesis).</summary>
public string? PrevHash { get; init; }
/// <summary>SHA-256 hash of this entry's canonical JSON form.</summary>
public required string VerdictHash { get; init; }
/// <summary>When this entry was created (UTC).</summary>
public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow;
/// <summary>Tenant identifier for multi-tenancy.</summary>
public Guid TenantId { get; init; }
}
/// <summary>
/// Verdict decision enum.
/// </summary>
public enum VerdictDecision
{
/// <summary>Verdict not yet determined.</summary>
Unknown = 0,
/// <summary>Approved for release.</summary>
Approve = 1,
/// <summary>Rejected - do not release.</summary>
Reject = 2,
/// <summary>Pending human review.</summary>
Pending = 3
}

View File

@@ -0,0 +1,75 @@
-- -----------------------------------------------------------------------------
-- 001_create_verdict_ledger.sql
-- Sprint: SPRINT_20260118_015_Attestor_verdict_ledger_foundation
-- Task: VL-001 - Create VerdictLedger database schema
-- Description: Append-only verdict ledger with SHA-256 hash chaining
-- -----------------------------------------------------------------------------
-- Create decision enum
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'verdict_decision') THEN
CREATE TYPE verdict_decision AS ENUM ('unknown', 'approve', 'reject', 'pending');
END IF;
END$$;
-- Create verdict_ledger table
CREATE TABLE IF NOT EXISTS verdict_ledger (
-- Primary identifier
ledger_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Package/artifact reference
bom_ref VARCHAR(2048) NOT NULL,
-- CycloneDX serial number (URN format)
cyclonedx_serial VARCHAR(256),
-- Transparency log reference (populated after Rekor submission)
rekor_uuid VARCHAR(128),
-- Verdict decision
decision verdict_decision NOT NULL DEFAULT 'unknown',
-- Human-readable reason for decision
reason TEXT,
-- Policy configuration reference
policy_bundle_id VARCHAR(256) NOT NULL,
policy_bundle_hash VARCHAR(64) NOT NULL, -- SHA-256 hex
-- Verifier provenance
verifier_image_digest VARCHAR(256) NOT NULL,
-- Signing key reference
signer_keyid VARCHAR(256) NOT NULL,
-- Hash chain fields (append-only integrity)
prev_hash VARCHAR(64), -- NULL for genesis entry
verdict_hash VARCHAR(64) NOT NULL, -- SHA-256 of canonical entry
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Multi-tenancy
tenant_id UUID NOT NULL,
-- Constraints
CONSTRAINT uq_verdict_hash UNIQUE (verdict_hash)
);
-- Indexes for common query patterns
CREATE INDEX IF NOT EXISTS idx_verdict_ledger_bom_ref ON verdict_ledger (bom_ref);
CREATE INDEX IF NOT EXISTS idx_verdict_ledger_rekor_uuid ON verdict_ledger (rekor_uuid) WHERE rekor_uuid IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_verdict_ledger_created_at ON verdict_ledger (created_at DESC);
CREATE INDEX IF NOT EXISTS idx_verdict_ledger_tenant ON verdict_ledger (tenant_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_verdict_ledger_decision ON verdict_ledger (decision, created_at DESC);
-- Comments
COMMENT ON TABLE verdict_ledger IS 'Append-only cryptographic audit trail for release verdicts';
COMMENT ON COLUMN verdict_ledger.prev_hash IS 'SHA-256 of previous entry; NULL for genesis';
COMMENT ON COLUMN verdict_ledger.verdict_hash IS 'SHA-256 of canonical JSON representation of this entry';
-- Revoke UPDATE/DELETE for application role (enforce append-only)
-- Note: Run this after creating the application role
-- REVOKE UPDATE, DELETE ON verdict_ledger FROM stella_app;
-- GRANT INSERT, SELECT ON verdict_ledger TO stella_app;

View File

@@ -0,0 +1,83 @@
-- -----------------------------------------------------------------------------
-- 001_verdict_ledger_initial.sql
-- Sprint: SPRINT_20260118_015_Attestor_verdict_ledger_foundation
-- Task: VL-001 - Create VerdictLedger database schema
-- Description: Append-only verdict ledger with SHA-256 hash chaining
-- -----------------------------------------------------------------------------
-- Create verdict decision enum
DO $$ BEGIN
CREATE TYPE verdict_decision AS ENUM ('unknown', 'approve', 'reject', 'pending');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Create the verdict_ledger table
CREATE TABLE IF NOT EXISTS verdict_ledger (
ledger_id UUID PRIMARY KEY,
bom_ref VARCHAR(2048) NOT NULL,
cyclonedx_serial VARCHAR(512),
rekor_uuid VARCHAR(128),
decision verdict_decision NOT NULL DEFAULT 'unknown',
reason TEXT NOT NULL,
policy_bundle_id VARCHAR(256) NOT NULL,
policy_bundle_hash VARCHAR(64) NOT NULL,
verifier_image_digest VARCHAR(256) NOT NULL,
signer_keyid VARCHAR(512) NOT NULL,
prev_hash VARCHAR(64), -- SHA-256 hex, null for genesis entry
verdict_hash VARCHAR(64) NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
tenant_id UUID NOT NULL,
-- Constraints
CONSTRAINT verdict_hash_format CHECK (verdict_hash ~ '^[a-f0-9]{64}$'),
CONSTRAINT prev_hash_format CHECK (prev_hash IS NULL OR prev_hash ~ '^[a-f0-9]{64}$'),
CONSTRAINT policy_hash_format CHECK (policy_bundle_hash ~ '^[a-f0-9]{64}$')
);
-- Indexes for common query patterns
CREATE INDEX IF NOT EXISTS idx_verdict_ledger_bom_ref
ON verdict_ledger (bom_ref);
CREATE INDEX IF NOT EXISTS idx_verdict_ledger_rekor_uuid
ON verdict_ledger (rekor_uuid)
WHERE rekor_uuid IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_verdict_ledger_created_at
ON verdict_ledger (created_at DESC);
CREATE INDEX IF NOT EXISTS idx_verdict_ledger_tenant_created
ON verdict_ledger (tenant_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_verdict_ledger_prev_hash
ON verdict_ledger (prev_hash)
WHERE prev_hash IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_verdict_ledger_decision
ON verdict_ledger (decision);
-- Composite index for chain walking
CREATE INDEX IF NOT EXISTS idx_verdict_ledger_chain
ON verdict_ledger (tenant_id, verdict_hash);
-- Comments
COMMENT ON TABLE verdict_ledger IS 'Append-only ledger of release verdicts with SHA-256 hash chaining for cryptographic audit trail';
COMMENT ON COLUMN verdict_ledger.ledger_id IS 'Unique identifier for this ledger entry';
COMMENT ON COLUMN verdict_ledger.bom_ref IS 'Package URL (purl) or container digest reference';
COMMENT ON COLUMN verdict_ledger.cyclonedx_serial IS 'CycloneDX serialNumber URN linking to SBOM';
COMMENT ON COLUMN verdict_ledger.rekor_uuid IS 'Transparency log entry UUID for external verification';
COMMENT ON COLUMN verdict_ledger.decision IS 'The release decision: unknown, approve, reject, or pending';
COMMENT ON COLUMN verdict_ledger.reason IS 'Human-readable explanation for the decision';
COMMENT ON COLUMN verdict_ledger.policy_bundle_id IS 'Reference to the policy configuration used';
COMMENT ON COLUMN verdict_ledger.policy_bundle_hash IS 'SHA-256 hash of the policy bundle for reproducibility';
COMMENT ON COLUMN verdict_ledger.verifier_image_digest IS 'Container digest of the verifier service';
COMMENT ON COLUMN verdict_ledger.signer_keyid IS 'Key ID that signed this verdict';
COMMENT ON COLUMN verdict_ledger.prev_hash IS 'SHA-256 hash of previous entry (null for genesis)';
COMMENT ON COLUMN verdict_ledger.verdict_hash IS 'SHA-256 hash of this entry canonical JSON form';
COMMENT ON COLUMN verdict_ledger.created_at IS 'Timestamp when this verdict was recorded';
COMMENT ON COLUMN verdict_ledger.tenant_id IS 'Tenant identifier for multi-tenancy';
-- Revoke UPDATE and DELETE for application role (append-only enforcement)
-- This should be run after creating the appropriate role
-- REVOKE UPDATE, DELETE ON verdict_ledger FROM stellaops_app;
-- GRANT INSERT, SELECT ON verdict_ledger TO stellaops_app;

View File

@@ -0,0 +1,97 @@
// -----------------------------------------------------------------------------
// IVerdictLedgerRepository.cs
// Sprint: SPRINT_20260118_015_Attestor_verdict_ledger_foundation
// Task: VL-002 - Implement VerdictLedger entity and repository
// Description: Repository interface for append-only verdict ledger
// -----------------------------------------------------------------------------
using StellaOps.Attestor.Persistence.Entities;
namespace StellaOps.Attestor.Persistence.Repositories;
/// <summary>
/// Repository for append-only verdict ledger operations.
/// Enforces hash chain integrity on append operations.
/// </summary>
public interface IVerdictLedgerRepository
{
/// <summary>
/// Appends a new entry to the ledger.
/// </summary>
/// <param name="entry">The entry to append.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The appended entry with generated fields populated.</returns>
/// <exception cref="ChainIntegrityException">
/// Thrown if entry.PrevHash doesn't match the latest entry's VerdictHash.
/// </exception>
Task<VerdictLedgerEntry> AppendAsync(VerdictLedgerEntry entry, CancellationToken ct = default);
/// <summary>
/// Gets an entry by its verdict hash.
/// </summary>
/// <param name="verdictHash">SHA-256 hash of the entry.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The entry if found, null otherwise.</returns>
Task<VerdictLedgerEntry?> GetByHashAsync(string verdictHash, CancellationToken ct = default);
/// <summary>
/// Gets all entries for a given bom-ref.
/// </summary>
/// <param name="bomRef">Package URL or container digest.</param>
/// <param name="tenantId">Tenant identifier.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Entries ordered by creation time (oldest first).</returns>
Task<IReadOnlyList<VerdictLedgerEntry>> GetByBomRefAsync(
string bomRef,
Guid tenantId,
CancellationToken ct = default);
/// <summary>
/// Gets the latest entry for a tenant (tip of the chain).
/// </summary>
/// <param name="tenantId">Tenant identifier.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The latest entry if any exist, null otherwise.</returns>
Task<VerdictLedgerEntry?> GetLatestAsync(Guid tenantId, CancellationToken ct = default);
/// <summary>
/// Gets entries in a hash range for chain verification.
/// </summary>
/// <param name="tenantId">Tenant identifier.</param>
/// <param name="fromHash">Starting hash (inclusive).</param>
/// <param name="toHash">Ending hash (inclusive).</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Entries in chain order from fromHash to toHash.</returns>
Task<IReadOnlyList<VerdictLedgerEntry>> GetChainAsync(
Guid tenantId,
string fromHash,
string toHash,
CancellationToken ct = default);
/// <summary>
/// Counts total entries for a tenant.
/// </summary>
Task<long> CountAsync(Guid tenantId, CancellationToken ct = default);
}
/// <summary>
/// Exception thrown when hash chain integrity is violated.
/// </summary>
public sealed class ChainIntegrityException : Exception
{
/// <summary>Expected previous hash.</summary>
public string? ExpectedPrevHash { get; }
/// <summary>Actual previous hash provided.</summary>
public string? ActualPrevHash { get; }
/// <summary>
/// Creates a new chain integrity exception.
/// </summary>
public ChainIntegrityException(string? expected, string? actual)
: base($"Chain integrity violation: expected prev_hash '{expected ?? "(genesis)"}' but got '{actual ?? "(genesis)"}'")
{
ExpectedPrevHash = expected;
ActualPrevHash = actual;
}
}

View File

@@ -0,0 +1,240 @@
// -----------------------------------------------------------------------------
// PostgresVerdictLedgerRepository.cs
// Sprint: SPRINT_20260118_015_Attestor_verdict_ledger_foundation
// Task: VL-002 - Implement VerdictLedger entity and repository
// Description: PostgreSQL implementation of verdict ledger repository
// -----------------------------------------------------------------------------
using Npgsql;
using StellaOps.Attestor.Persistence.Entities;
namespace StellaOps.Attestor.Persistence.Repositories;
/// <summary>
/// PostgreSQL implementation of the verdict ledger repository.
/// Enforces append-only semantics with hash chain validation.
/// </summary>
public sealed class PostgresVerdictLedgerRepository : IVerdictLedgerRepository
{
private readonly string _connectionString;
/// <summary>
/// Creates a new PostgreSQL verdict ledger repository.
/// </summary>
public PostgresVerdictLedgerRepository(string connectionString)
{
_connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
}
/// <inheritdoc />
public async Task<VerdictLedgerEntry> AppendAsync(VerdictLedgerEntry entry, CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(entry);
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync(ct);
// Validate chain integrity
var latest = await GetLatestAsync(entry.TenantId, ct);
var expectedPrevHash = latest?.VerdictHash;
if (entry.PrevHash != expectedPrevHash)
{
throw new ChainIntegrityException(expectedPrevHash, entry.PrevHash);
}
// Insert the new entry
const string sql = @"
INSERT INTO verdict_ledger (
ledger_id, bom_ref, cyclonedx_serial, rekor_uuid, decision, reason,
policy_bundle_id, policy_bundle_hash, verifier_image_digest, signer_keyid,
prev_hash, verdict_hash, created_at, tenant_id
) VALUES (
@ledger_id, @bom_ref, @cyclonedx_serial, @rekor_uuid, @decision::verdict_decision, @reason,
@policy_bundle_id, @policy_bundle_hash, @verifier_image_digest, @signer_keyid,
@prev_hash, @verdict_hash, @created_at, @tenant_id
)
RETURNING ledger_id, created_at";
await using var cmd = new NpgsqlCommand(sql, conn);
cmd.Parameters.AddWithValue("ledger_id", entry.LedgerId);
cmd.Parameters.AddWithValue("bom_ref", entry.BomRef);
cmd.Parameters.AddWithValue("cyclonedx_serial", (object?)entry.CycloneDxSerial ?? DBNull.Value);
cmd.Parameters.AddWithValue("rekor_uuid", (object?)entry.RekorUuid ?? DBNull.Value);
cmd.Parameters.AddWithValue("decision", entry.Decision.ToString().ToLowerInvariant());
cmd.Parameters.AddWithValue("reason", (object?)entry.Reason ?? DBNull.Value);
cmd.Parameters.AddWithValue("policy_bundle_id", entry.PolicyBundleId);
cmd.Parameters.AddWithValue("policy_bundle_hash", entry.PolicyBundleHash);
cmd.Parameters.AddWithValue("verifier_image_digest", entry.VerifierImageDigest);
cmd.Parameters.AddWithValue("signer_keyid", entry.SignerKeyId);
cmd.Parameters.AddWithValue("prev_hash", (object?)entry.PrevHash ?? DBNull.Value);
cmd.Parameters.AddWithValue("verdict_hash", entry.VerdictHash);
cmd.Parameters.AddWithValue("created_at", entry.CreatedAt);
cmd.Parameters.AddWithValue("tenant_id", entry.TenantId);
await using var reader = await cmd.ExecuteReaderAsync(ct);
if (await reader.ReadAsync(ct))
{
return entry with
{
LedgerId = reader.GetGuid(0),
CreatedAt = reader.GetDateTime(1)
};
}
throw new InvalidOperationException("Insert failed to return ledger_id");
}
/// <inheritdoc />
public async Task<VerdictLedgerEntry?> GetByHashAsync(string verdictHash, CancellationToken ct = default)
{
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync(ct);
const string sql = @"
SELECT ledger_id, bom_ref, cyclonedx_serial, rekor_uuid, decision, reason,
policy_bundle_id, policy_bundle_hash, verifier_image_digest, signer_keyid,
prev_hash, verdict_hash, created_at, tenant_id
FROM verdict_ledger
WHERE verdict_hash = @verdict_hash";
await using var cmd = new NpgsqlCommand(sql, conn);
cmd.Parameters.AddWithValue("verdict_hash", verdictHash);
await using var reader = await cmd.ExecuteReaderAsync(ct);
if (await reader.ReadAsync(ct))
{
return MapToEntry(reader);
}
return null;
}
/// <inheritdoc />
public async Task<IReadOnlyList<VerdictLedgerEntry>> GetByBomRefAsync(
string bomRef,
Guid tenantId,
CancellationToken ct = default)
{
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync(ct);
const string sql = @"
SELECT ledger_id, bom_ref, cyclonedx_serial, rekor_uuid, decision, reason,
policy_bundle_id, policy_bundle_hash, verifier_image_digest, signer_keyid,
prev_hash, verdict_hash, created_at, tenant_id
FROM verdict_ledger
WHERE bom_ref = @bom_ref AND tenant_id = @tenant_id
ORDER BY created_at ASC";
await using var cmd = new NpgsqlCommand(sql, conn);
cmd.Parameters.AddWithValue("bom_ref", bomRef);
cmd.Parameters.AddWithValue("tenant_id", tenantId);
var results = new List<VerdictLedgerEntry>();
await using var reader = await cmd.ExecuteReaderAsync(ct);
while (await reader.ReadAsync(ct))
{
results.Add(MapToEntry(reader));
}
return results;
}
/// <inheritdoc />
public async Task<VerdictLedgerEntry?> GetLatestAsync(Guid tenantId, CancellationToken ct = default)
{
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync(ct);
const string sql = @"
SELECT ledger_id, bom_ref, cyclonedx_serial, rekor_uuid, decision, reason,
policy_bundle_id, policy_bundle_hash, verifier_image_digest, signer_keyid,
prev_hash, verdict_hash, created_at, tenant_id
FROM verdict_ledger
WHERE tenant_id = @tenant_id
ORDER BY created_at DESC
LIMIT 1";
await using var cmd = new NpgsqlCommand(sql, conn);
cmd.Parameters.AddWithValue("tenant_id", tenantId);
await using var reader = await cmd.ExecuteReaderAsync(ct);
if (await reader.ReadAsync(ct))
{
return MapToEntry(reader);
}
return null;
}
/// <inheritdoc />
public async Task<IReadOnlyList<VerdictLedgerEntry>> GetChainAsync(
Guid tenantId,
string fromHash,
string toHash,
CancellationToken ct = default)
{
// Walk backward from toHash to fromHash
var chain = new List<VerdictLedgerEntry>();
var currentHash = toHash;
while (!string.IsNullOrEmpty(currentHash))
{
var entry = await GetByHashAsync(currentHash, ct);
if (entry == null || entry.TenantId != tenantId)
{
break;
}
chain.Add(entry);
if (currentHash == fromHash)
{
break;
}
currentHash = entry.PrevHash!;
}
// Return in chain order (oldest to newest)
chain.Reverse();
return chain;
}
/// <inheritdoc />
public async Task<long> CountAsync(Guid tenantId, CancellationToken ct = default)
{
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync(ct);
const string sql = "SELECT COUNT(*) FROM verdict_ledger WHERE tenant_id = @tenant_id";
await using var cmd = new NpgsqlCommand(sql, conn);
cmd.Parameters.AddWithValue("tenant_id", tenantId);
var result = await cmd.ExecuteScalarAsync(ct);
return Convert.ToInt64(result);
}
private static VerdictLedgerEntry MapToEntry(NpgsqlDataReader reader)
{
return new VerdictLedgerEntry
{
LedgerId = reader.GetGuid(0),
BomRef = reader.GetString(1),
CycloneDxSerial = reader.IsDBNull(2) ? null : reader.GetString(2),
RekorUuid = reader.IsDBNull(3) ? null : reader.GetString(3),
Decision = Enum.Parse<VerdictDecision>(reader.GetString(4), ignoreCase: true),
Reason = reader.IsDBNull(5) ? null : reader.GetString(5),
PolicyBundleId = reader.GetString(6),
PolicyBundleHash = reader.GetString(7),
VerifierImageDigest = reader.GetString(8),
SignerKeyId = reader.GetString(9),
PrevHash = reader.IsDBNull(10) ? null : reader.GetString(10),
VerdictHash = reader.GetString(11),
CreatedAt = reader.GetDateTime(12),
TenantId = reader.GetGuid(13)
};
}
}