Gaps fill up, fixes, ui restructuring
This commit is contained in:
@@ -19,9 +19,11 @@ using StellaOps.Attestor.Core.Storage;
|
||||
using StellaOps.Attestor.Core.Submission;
|
||||
using StellaOps.Attestor.Core.Verification;
|
||||
using StellaOps.Attestor.Infrastructure;
|
||||
using StellaOps.Attestor.Persistence;
|
||||
using StellaOps.Attestor.ProofChain;
|
||||
using StellaOps.Attestor.Spdx3;
|
||||
using StellaOps.Attestor.Watchlist;
|
||||
using StellaOps.Attestor.WebService.Endpoints;
|
||||
using StellaOps.Attestor.WebService.Options;
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using StellaOps.Configuration;
|
||||
@@ -141,6 +143,13 @@ internal static class AttestorWebServiceComposition
|
||||
builder.Services.AddAttestorInfrastructure();
|
||||
builder.Services.AddProofChainServices();
|
||||
|
||||
// Predicate type registry (Sprint: SPRINT_20260219_010, PSR-02)
|
||||
var postgresConnectionString = builder.Configuration["attestor:postgres:connectionString"];
|
||||
if (!string.IsNullOrWhiteSpace(postgresConnectionString))
|
||||
{
|
||||
builder.Services.AddPredicateTypeRegistry(postgresConnectionString);
|
||||
}
|
||||
|
||||
builder.Services.AddScoped<Services.IProofChainQueryService, Services.ProofChainQueryService>();
|
||||
builder.Services.AddScoped<Services.IProofVerificationService, Services.ProofVerificationService>();
|
||||
|
||||
@@ -410,6 +419,7 @@ internal static class AttestorWebServiceComposition
|
||||
app.MapControllers();
|
||||
app.MapAttestorEndpoints(attestorOptions);
|
||||
app.MapWatchlistEndpoints();
|
||||
app.MapPredicateRegistryEndpoints();
|
||||
|
||||
app.TryRefreshStellaRouterEndpoints(routerOptions);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// PredicateRegistryEndpoints.cs
|
||||
// Sprint: SPRINT_20260219_010 (PSR-02)
|
||||
// Task: PSR-02 - Create Predicate Schema Registry endpoints and repository
|
||||
// Description: REST API endpoints for the predicate type schema registry
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using StellaOps.Attestor.Persistence.Repositories;
|
||||
|
||||
namespace StellaOps.Attestor.WebService.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Endpoints for the predicate type schema registry.
|
||||
/// Sprint: SPRINT_20260219_010 (PSR-02)
|
||||
/// </summary>
|
||||
public static class PredicateRegistryEndpoints
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps predicate registry endpoints.
|
||||
/// </summary>
|
||||
public static void MapPredicateRegistryEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
var group = app.MapGroup("/api/v1/attestor/predicates")
|
||||
.WithTags("Predicate Registry")
|
||||
.WithOpenApi();
|
||||
|
||||
group.MapGet("/", ListPredicateTypes)
|
||||
.WithName("ListPredicateTypes")
|
||||
.WithSummary("List all registered predicate types")
|
||||
.Produces<PredicateTypeListResponse>(StatusCodes.Status200OK);
|
||||
|
||||
group.MapGet("/{uri}", GetPredicateType)
|
||||
.WithName("GetPredicateType")
|
||||
.WithSummary("Get predicate type schema by URI")
|
||||
.Produces<PredicateTypeRegistryEntry>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status404NotFound);
|
||||
}
|
||||
|
||||
private static async Task<IResult> ListPredicateTypes(
|
||||
IPredicateTypeRegistryRepository repository,
|
||||
string? category = null,
|
||||
bool? isActive = null,
|
||||
int offset = 0,
|
||||
int limit = 100,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var entries = await repository.ListAsync(category, isActive, offset, limit, ct);
|
||||
return Results.Ok(new PredicateTypeListResponse
|
||||
{
|
||||
Items = entries,
|
||||
Offset = offset,
|
||||
Limit = limit,
|
||||
Count = entries.Count,
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task<IResult> GetPredicateType(
|
||||
string uri,
|
||||
IPredicateTypeRegistryRepository repository,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var decoded = Uri.UnescapeDataString(uri);
|
||||
var entry = await repository.GetByUriAsync(decoded, ct);
|
||||
if (entry is null)
|
||||
{
|
||||
return Results.NotFound(new { error = "Predicate type not found", uri = decoded });
|
||||
}
|
||||
|
||||
return Results.Ok(entry);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response for listing predicate types.
|
||||
/// </summary>
|
||||
public sealed record PredicateTypeListResponse
|
||||
{
|
||||
/// <summary>The predicate type entries.</summary>
|
||||
public required IReadOnlyList<PredicateTypeRegistryEntry> Items { get; init; }
|
||||
|
||||
/// <summary>Pagination offset.</summary>
|
||||
public int Offset { get; init; }
|
||||
|
||||
/// <summary>Pagination limit.</summary>
|
||||
public int Limit { get; init; }
|
||||
|
||||
/// <summary>Number of items returned.</summary>
|
||||
public int Count { get; init; }
|
||||
}
|
||||
@@ -32,5 +32,6 @@
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Attestor.Bundling\StellaOps.Attestor.Bundling.csproj" />
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Attestor.Spdx3\StellaOps.Attestor.Spdx3.csproj" />
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Attestor.Watchlist\StellaOps.Attestor.Watchlist.csproj" />
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Attestor.Persistence\StellaOps.Attestor.Persistence.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
-- Attestor Schema Migration 002: Predicate Type Registry
|
||||
-- Sprint: SPRINT_20260219_010 (PSR-01)
|
||||
-- Creates discoverable, versioned registry for all predicate types
|
||||
|
||||
-- ============================================================================
|
||||
-- Predicate Type Registry Table
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS proofchain.predicate_type_registry (
|
||||
registry_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
predicate_type_uri TEXT NOT NULL,
|
||||
display_name TEXT NOT NULL,
|
||||
version TEXT NOT NULL DEFAULT '1.0.0',
|
||||
category TEXT NOT NULL DEFAULT 'stella-core'
|
||||
CHECK (category IN ('stella-core', 'stella-proof', 'stella-delta', 'ecosystem', 'intoto', 'custom')),
|
||||
json_schema JSONB,
|
||||
description TEXT,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
validation_mode TEXT NOT NULL DEFAULT 'log-only'
|
||||
CHECK (validation_mode IN ('log-only', 'warn', 'reject')),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT uq_predicate_type_version UNIQUE (predicate_type_uri, version)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_predicate_registry_uri
|
||||
ON proofchain.predicate_type_registry(predicate_type_uri);
|
||||
CREATE INDEX IF NOT EXISTS idx_predicate_registry_category
|
||||
ON proofchain.predicate_type_registry(category);
|
||||
CREATE INDEX IF NOT EXISTS idx_predicate_registry_active
|
||||
ON proofchain.predicate_type_registry(is_active) WHERE is_active = TRUE;
|
||||
|
||||
-- Apply updated_at trigger
|
||||
DROP TRIGGER IF EXISTS update_predicate_registry_updated_at ON proofchain.predicate_type_registry;
|
||||
CREATE TRIGGER update_predicate_registry_updated_at
|
||||
BEFORE UPDATE ON proofchain.predicate_type_registry
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION proofchain.update_updated_at_column();
|
||||
|
||||
COMMENT ON TABLE proofchain.predicate_type_registry IS 'Discoverable registry of all predicate types accepted by the Attestor';
|
||||
COMMENT ON COLUMN proofchain.predicate_type_registry.predicate_type_uri IS 'Canonical URI for the predicate type (e.g., https://stella-ops.org/predicates/evidence/v1)';
|
||||
COMMENT ON COLUMN proofchain.predicate_type_registry.validation_mode IS 'How mismatches are handled: log-only (default), warn, or reject';
|
||||
|
||||
-- ============================================================================
|
||||
-- Seed: stella-core predicates
|
||||
-- ============================================================================
|
||||
|
||||
INSERT INTO proofchain.predicate_type_registry (predicate_type_uri, display_name, version, category, description) VALUES
|
||||
('https://stella-ops.org/predicates/sbom-linkage/v1', 'SBOM Linkage', '1.0.0', 'stella-core', 'Links SBOM components to evidence and proof spines'),
|
||||
('https://stella-ops.org/predicates/vex-verdict/v1', 'VEX Verdict', '1.0.0', 'stella-core', 'VEX consensus verdict for an artifact+advisory tuple'),
|
||||
('https://stella-ops.org/predicates/evidence/v1', 'Evidence', '1.0.0', 'stella-core', 'Generic evidence attestation linking scan results to artifacts'),
|
||||
('https://stella-ops.org/predicates/reasoning/v1', 'Reasoning', '1.0.0', 'stella-core', 'Policy reasoning chain for a release decision'),
|
||||
('https://stella-ops.org/predicates/proof-spine/v1', 'Proof Spine', '1.0.0', 'stella-core', 'Merkle-aggregated proof spine linking evidence to verdicts'),
|
||||
('https://stella-ops.org/predicates/reachability-drift/v1', 'Reachability Drift', '1.0.0', 'stella-core', 'Reachability state changes between consecutive scans'),
|
||||
('https://stella-ops.org/predicates/reachability-subgraph/v1', 'Reachability Subgraph', '1.0.0', 'stella-core', 'Call graph subgraph for a specific vulnerability path'),
|
||||
('https://stella-ops.org/predicates/delta-verdict/v1', 'Delta Verdict', '1.0.0', 'stella-core', 'Verdict differences between two scan runs'),
|
||||
('https://stella-ops.org/predicates/policy-decision/v1', 'Policy Decision', '1.0.0', 'stella-core', 'Policy engine evaluation result for a release gate'),
|
||||
('https://stella-ops.org/predicates/unknowns-budget/v1', 'Unknowns Budget', '1.0.0', 'stella-core', 'Budget check for unknown reachability components'),
|
||||
('https://stella-ops.org/predicates/ai-code-guard/v1', 'AI Code Guard', '1.0.0', 'stella-core', 'AI-assisted code security analysis results'),
|
||||
('https://stella-ops.org/predicates/fix-chain/v1', 'Fix Chain', '1.0.0', 'stella-core', 'Linked chain of fix commits from vulnerability to resolution'),
|
||||
('https://stella-ops.org/attestation/graph-root/v1', 'Graph Root', '1.0.0', 'stella-core', 'Root attestation for a complete call graph')
|
||||
ON CONFLICT (predicate_type_uri, version) DO NOTHING;
|
||||
|
||||
-- ============================================================================
|
||||
-- Seed: stella-proof predicates (ProofChain)
|
||||
-- ============================================================================
|
||||
|
||||
INSERT INTO proofchain.predicate_type_registry (predicate_type_uri, display_name, version, category, description) VALUES
|
||||
('https://stella.ops/predicates/path-witness/v1', 'Path Witness', '1.0.0', 'stella-proof', 'Entrypoint-to-sink call path witness with gate detection'),
|
||||
('https://stella.ops/predicates/runtime-witness/v1', 'Runtime Witness', '1.0.0', 'stella-proof', 'Runtime micro-witness from eBPF/ETW observations'),
|
||||
('https://stella.ops/predicates/policy-decision@v2', 'Policy Decision v2', '2.0.0', 'stella-proof', 'Enhanced policy decision with reachability context'),
|
||||
('https://stellaops.dev/predicates/binary-micro-witness@v1', 'Binary Micro-Witness', '1.0.0', 'stella-proof', 'Binary-level micro-witness with build ID correlation'),
|
||||
('https://stellaops.dev/predicates/binary-fingerprint-evidence@v1', 'Binary Fingerprint', '1.0.0', 'stella-proof', 'Binary fingerprint evidence for patch detection'),
|
||||
('https://stellaops.io/attestation/budget-check/v1', 'Budget Check', '1.0.0', 'stella-proof', 'Unknowns budget check attestation'),
|
||||
('https://stellaops.dev/attestation/vex/v1', 'VEX Attestation', '1.0.0', 'stella-proof', 'DSSE-signed VEX statement attestation'),
|
||||
('https://stellaops.dev/attestations/vex-override/v1', 'VEX Override', '1.0.0', 'stella-proof', 'Manual VEX override decision with justification'),
|
||||
('https://stellaops.dev/predicates/trust-verdict@v1', 'Trust Verdict', '1.0.0', 'stella-proof', 'Trust lattice verdict combining P/C/R vectors'),
|
||||
('https://stellaops.io/attestation/v1/signed-exception', 'Signed Exception', '1.0.0', 'stella-proof', 'Manually approved exception with expiry'),
|
||||
('https://stellaops.dev/attestation/verification-report/v1', 'Verification Report', '1.0.0', 'stella-proof', 'QA verification report attestation')
|
||||
ON CONFLICT (predicate_type_uri, version) DO NOTHING;
|
||||
|
||||
-- ============================================================================
|
||||
-- Seed: stella-delta predicates
|
||||
-- ============================================================================
|
||||
|
||||
INSERT INTO proofchain.predicate_type_registry (predicate_type_uri, display_name, version, category, description) VALUES
|
||||
('stella.ops/changetrace@v1', 'Change Trace', '1.0.0', 'stella-delta', 'File-level change trace between SBOM versions'),
|
||||
('stella.ops/vex-delta@v1', 'VEX Delta', '1.0.0', 'stella-delta', 'VEX statement differences between consecutive ingestions'),
|
||||
('stella.ops/sbom-delta@v1', 'SBOM Delta', '1.0.0', 'stella-delta', 'Component differences between two SBOM versions'),
|
||||
('stella.ops/verdict-delta@v1', 'Verdict Delta', '1.0.0', 'stella-delta', 'Verdict changes between policy evaluations'),
|
||||
('stellaops.binarydiff.v1', 'Binary Diff', '1.0.0', 'stella-delta', 'Binary diff signatures for patch detection')
|
||||
ON CONFLICT (predicate_type_uri, version) DO NOTHING;
|
||||
|
||||
-- ============================================================================
|
||||
-- Seed: ecosystem predicates
|
||||
-- ============================================================================
|
||||
|
||||
INSERT INTO proofchain.predicate_type_registry (predicate_type_uri, display_name, version, category, description) VALUES
|
||||
('https://spdx.dev/Document', 'SPDX Document', '2.3.0', 'ecosystem', 'SPDX 2.x document attestation'),
|
||||
('https://cyclonedx.org/bom', 'CycloneDX BOM', '1.7.0', 'ecosystem', 'CycloneDX BOM attestation'),
|
||||
('https://slsa.dev/provenance', 'SLSA Provenance', '1.0.0', 'ecosystem', 'SLSA v1.0 build provenance')
|
||||
ON CONFLICT (predicate_type_uri, version) DO NOTHING;
|
||||
|
||||
-- ============================================================================
|
||||
-- Seed: in-toto standard predicates
|
||||
-- ============================================================================
|
||||
|
||||
INSERT INTO proofchain.predicate_type_registry (predicate_type_uri, display_name, version, category, description) VALUES
|
||||
('https://in-toto.io/Statement/v1', 'In-Toto Statement', '1.0.0', 'intoto', 'In-toto attestation statement wrapper'),
|
||||
('https://in-toto.io/Link/v1', 'In-Toto Link', '1.0.0', 'intoto', 'In-toto supply chain link'),
|
||||
('https://in-toto.io/Layout/v1', 'In-Toto Layout', '1.0.0', 'intoto', 'In-toto supply chain layout')
|
||||
ON CONFLICT (predicate_type_uri, version) DO NOTHING;
|
||||
@@ -0,0 +1,42 @@
|
||||
-- Migration 003: Artifact Canonical Record materialized view
|
||||
-- Sprint: SPRINT_20260219_009 (CID-04)
|
||||
-- Purpose: Unified read projection joining sbom_entries + dsse_envelopes + rekor_entries
|
||||
-- for the Evidence Thread API (GET /api/v1/evidence/thread/{canonical_id}).
|
||||
|
||||
-- Materialized view: one row per canonical_id with aggregated attestation evidence.
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS proofchain.artifact_canonical_records AS
|
||||
SELECT
|
||||
se.bom_digest AS canonical_id,
|
||||
'cyclonedx-jcs:1'::text AS format,
|
||||
se.artifact_digest,
|
||||
se.purl,
|
||||
se.created_at,
|
||||
COALESCE(
|
||||
jsonb_agg(
|
||||
DISTINCT jsonb_build_object(
|
||||
'predicate_type', de.predicate_type,
|
||||
'dsse_digest', de.body_hash,
|
||||
'signer_keyid', de.signer_keyid,
|
||||
'rekor_entry_id', re.uuid,
|
||||
'rekor_tile', re.log_id,
|
||||
'signed_at', de.signed_at
|
||||
)
|
||||
) FILTER (WHERE de.env_id IS NOT NULL),
|
||||
'[]'::jsonb
|
||||
) AS attestations
|
||||
FROM proofchain.sbom_entries se
|
||||
LEFT JOIN proofchain.dsse_envelopes de ON de.entry_id = se.entry_id
|
||||
LEFT JOIN proofchain.rekor_entries re ON re.env_id = de.env_id
|
||||
GROUP BY se.entry_id, se.bom_digest, se.artifact_digest, se.purl, se.created_at;
|
||||
|
||||
-- Unique index for CONCURRENTLY refresh and fast lookup.
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_acr_canonical_id
|
||||
ON proofchain.artifact_canonical_records (canonical_id);
|
||||
|
||||
-- Index for PURL-based lookup (Evidence Thread by PURL).
|
||||
CREATE INDEX IF NOT EXISTS idx_acr_purl
|
||||
ON proofchain.artifact_canonical_records (purl)
|
||||
WHERE purl IS NOT NULL;
|
||||
|
||||
COMMENT ON MATERIALIZED VIEW proofchain.artifact_canonical_records IS
|
||||
'Unified read projection for the Evidence Thread API. Joins SBOM entries, DSSE envelopes, and Rekor entries into one row per canonical_id. Refresh via REFRESH MATERIALIZED VIEW CONCURRENTLY.';
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using StellaOps.Attestor.Persistence.Repositories;
|
||||
using System.Diagnostics.Metrics;
|
||||
|
||||
namespace StellaOps.Attestor.Persistence;
|
||||
@@ -28,4 +29,18 @@ public static class PersistenceServiceCollectionExtensions
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers the predicate type registry repository backed by PostgreSQL.
|
||||
/// Sprint: SPRINT_20260219_010 (PSR-02)
|
||||
/// </summary>
|
||||
public static IServiceCollection AddPredicateTypeRegistry(
|
||||
this IServiceCollection services,
|
||||
string connectionString)
|
||||
{
|
||||
services.TryAddSingleton<IPredicateTypeRegistryRepository>(
|
||||
new PostgresPredicateTypeRegistryRepository(connectionString));
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// IPredicateTypeRegistryRepository.cs
|
||||
// Sprint: SPRINT_20260219_010 (PSR-02)
|
||||
// Task: PSR-02 - Create Predicate Schema Registry endpoints and repository
|
||||
// Description: Repository interface for predicate type registry lookups and management
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
namespace StellaOps.Attestor.Persistence.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository for predicate type registry lookups and management.
|
||||
/// Sprint: SPRINT_20260219_010 (PSR-02)
|
||||
/// </summary>
|
||||
public interface IPredicateTypeRegistryRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Lists predicate type entries with optional filtering.
|
||||
/// </summary>
|
||||
/// <param name="category">Optional category filter.</param>
|
||||
/// <param name="isActive">Optional active status filter.</param>
|
||||
/// <param name="offset">Pagination offset.</param>
|
||||
/// <param name="limit">Maximum entries to return.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Matching entries ordered by category and URI.</returns>
|
||||
Task<IReadOnlyList<PredicateTypeRegistryEntry>> ListAsync(
|
||||
string? category = null,
|
||||
bool? isActive = null,
|
||||
int offset = 0,
|
||||
int limit = 100,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a predicate type entry by its URI (latest version).
|
||||
/// </summary>
|
||||
/// <param name="predicateTypeUri">Canonical predicate type URI.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>The entry if found, null otherwise.</returns>
|
||||
Task<PredicateTypeRegistryEntry?> GetByUriAsync(
|
||||
string predicateTypeUri,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Registers a new predicate type entry (upsert on URI+version conflict).
|
||||
/// </summary>
|
||||
/// <param name="entry">The entry to register.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>The registered entry with generated fields populated.</returns>
|
||||
Task<PredicateTypeRegistryEntry> RegisterAsync(
|
||||
PredicateTypeRegistryEntry entry,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single entry in the predicate type registry.
|
||||
/// </summary>
|
||||
public sealed record PredicateTypeRegistryEntry
|
||||
{
|
||||
/// <summary>Primary key (UUID).</summary>
|
||||
public Guid RegistryId { get; init; }
|
||||
|
||||
/// <summary>Canonical URI for the predicate type.</summary>
|
||||
public required string PredicateTypeUri { get; init; }
|
||||
|
||||
/// <summary>Human-readable display name.</summary>
|
||||
public required string DisplayName { get; init; }
|
||||
|
||||
/// <summary>Semver version string.</summary>
|
||||
public string Version { get; init; } = "1.0.0";
|
||||
|
||||
/// <summary>Category: stella-core, stella-proof, stella-delta, ecosystem, intoto, custom.</summary>
|
||||
public string Category { get; init; } = "stella-core";
|
||||
|
||||
/// <summary>Optional JSON Schema for payload validation.</summary>
|
||||
public string? JsonSchema { get; init; }
|
||||
|
||||
/// <summary>Optional human-readable description.</summary>
|
||||
public string? Description { get; init; }
|
||||
|
||||
/// <summary>Whether this predicate type is currently active.</summary>
|
||||
public bool IsActive { get; init; } = true;
|
||||
|
||||
/// <summary>Validation mode: log-only, warn, or reject.</summary>
|
||||
public string ValidationMode { get; init; } = "log-only";
|
||||
|
||||
/// <summary>Creation timestamp.</summary>
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
|
||||
/// <summary>Last update timestamp.</summary>
|
||||
public DateTimeOffset UpdatedAt { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// PostgresPredicateTypeRegistryRepository.cs
|
||||
// Sprint: SPRINT_20260219_010 (PSR-02)
|
||||
// Task: PSR-02 - Create Predicate Schema Registry endpoints and repository
|
||||
// Description: PostgreSQL implementation of predicate type registry repository
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using Npgsql;
|
||||
|
||||
namespace StellaOps.Attestor.Persistence.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL-backed predicate type registry repository.
|
||||
/// Sprint: SPRINT_20260219_010 (PSR-02)
|
||||
/// </summary>
|
||||
public sealed class PostgresPredicateTypeRegistryRepository : IPredicateTypeRegistryRepository
|
||||
{
|
||||
private readonly string _connectionString;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new PostgreSQL predicate type registry repository.
|
||||
/// </summary>
|
||||
public PostgresPredicateTypeRegistryRepository(string connectionString)
|
||||
{
|
||||
_connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<PredicateTypeRegistryEntry>> ListAsync(
|
||||
string? category = null,
|
||||
bool? isActive = null,
|
||||
int offset = 0,
|
||||
int limit = 100,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
await using var conn = new NpgsqlConnection(_connectionString);
|
||||
await conn.OpenAsync(ct);
|
||||
|
||||
const string sql = @"
|
||||
SELECT registry_id, predicate_type_uri, display_name, version, category,
|
||||
json_schema, description, is_active, validation_mode, created_at, updated_at
|
||||
FROM proofchain.predicate_type_registry
|
||||
WHERE (@category::text IS NULL OR category = @category)
|
||||
AND (@is_active::boolean IS NULL OR is_active = @is_active)
|
||||
ORDER BY category, predicate_type_uri
|
||||
OFFSET @offset LIMIT @limit";
|
||||
|
||||
await using var cmd = new NpgsqlCommand(sql, conn);
|
||||
cmd.Parameters.AddWithValue("category", (object?)category ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("is_active", isActive.HasValue ? isActive.Value : DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("offset", offset);
|
||||
cmd.Parameters.AddWithValue("limit", limit);
|
||||
|
||||
await using var reader = await cmd.ExecuteReaderAsync(ct);
|
||||
var results = new List<PredicateTypeRegistryEntry>();
|
||||
|
||||
while (await reader.ReadAsync(ct))
|
||||
{
|
||||
results.Add(MapEntry(reader));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<PredicateTypeRegistryEntry?> GetByUriAsync(
|
||||
string predicateTypeUri,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
await using var conn = new NpgsqlConnection(_connectionString);
|
||||
await conn.OpenAsync(ct);
|
||||
|
||||
const string sql = @"
|
||||
SELECT registry_id, predicate_type_uri, display_name, version, category,
|
||||
json_schema, description, is_active, validation_mode, created_at, updated_at
|
||||
FROM proofchain.predicate_type_registry
|
||||
WHERE predicate_type_uri = @predicate_type_uri
|
||||
ORDER BY version DESC
|
||||
LIMIT 1";
|
||||
|
||||
await using var cmd = new NpgsqlCommand(sql, conn);
|
||||
cmd.Parameters.AddWithValue("predicate_type_uri", predicateTypeUri);
|
||||
|
||||
await using var reader = await cmd.ExecuteReaderAsync(ct);
|
||||
if (await reader.ReadAsync(ct))
|
||||
{
|
||||
return MapEntry(reader);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<PredicateTypeRegistryEntry> RegisterAsync(
|
||||
PredicateTypeRegistryEntry entry,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entry);
|
||||
|
||||
await using var conn = new NpgsqlConnection(_connectionString);
|
||||
await conn.OpenAsync(ct);
|
||||
|
||||
const string sql = @"
|
||||
INSERT INTO proofchain.predicate_type_registry
|
||||
(predicate_type_uri, display_name, version, category, json_schema, description, is_active, validation_mode)
|
||||
VALUES (@predicate_type_uri, @display_name, @version, @category, @json_schema::jsonb, @description, @is_active, @validation_mode)
|
||||
ON CONFLICT (predicate_type_uri, version) DO NOTHING
|
||||
RETURNING registry_id, created_at, updated_at";
|
||||
|
||||
await using var cmd = new NpgsqlCommand(sql, conn);
|
||||
cmd.Parameters.AddWithValue("predicate_type_uri", entry.PredicateTypeUri);
|
||||
cmd.Parameters.AddWithValue("display_name", entry.DisplayName);
|
||||
cmd.Parameters.AddWithValue("version", entry.Version);
|
||||
cmd.Parameters.AddWithValue("category", entry.Category);
|
||||
cmd.Parameters.AddWithValue("json_schema", (object?)entry.JsonSchema ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("description", (object?)entry.Description ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("is_active", entry.IsActive);
|
||||
cmd.Parameters.AddWithValue("validation_mode", entry.ValidationMode);
|
||||
|
||||
await using var reader = await cmd.ExecuteReaderAsync(ct);
|
||||
if (await reader.ReadAsync(ct))
|
||||
{
|
||||
return entry with
|
||||
{
|
||||
RegistryId = reader.GetGuid(0),
|
||||
CreatedAt = reader.GetDateTime(1),
|
||||
UpdatedAt = reader.GetDateTime(2),
|
||||
};
|
||||
}
|
||||
|
||||
// Conflict (already exists) - return existing
|
||||
var existing = await GetByUriAsync(entry.PredicateTypeUri, ct);
|
||||
return existing ?? entry;
|
||||
}
|
||||
|
||||
private static PredicateTypeRegistryEntry MapEntry(NpgsqlDataReader reader)
|
||||
{
|
||||
return new PredicateTypeRegistryEntry
|
||||
{
|
||||
RegistryId = reader.GetGuid(0),
|
||||
PredicateTypeUri = reader.GetString(1),
|
||||
DisplayName = reader.GetString(2),
|
||||
Version = reader.GetString(3),
|
||||
Category = reader.GetString(4),
|
||||
JsonSchema = reader.IsDBNull(5) ? null : reader.GetString(5),
|
||||
Description = reader.IsDBNull(6) ? null : reader.GetString(6),
|
||||
IsActive = reader.GetBoolean(7),
|
||||
ValidationMode = reader.GetString(8),
|
||||
CreatedAt = reader.GetDateTime(9),
|
||||
UpdatedAt = reader.GetDateTime(10),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
namespace StellaOps.Attestor.ProofChain.Predicates;
|
||||
|
||||
/// <summary>
|
||||
/// Predicate model for triage auto-suppress decisions.
|
||||
/// Emitted when a runtime witness confirms a VEX not_affected consensus
|
||||
/// with supporting unreachability evidence.
|
||||
/// Sprint: SPRINT_20260219_012 (MWS-01)
|
||||
/// </summary>
|
||||
public sealed record TriageSuppressPredicate
|
||||
{
|
||||
public const string PredicateTypeUri = "stella.ops/triageSuppress@v1";
|
||||
|
||||
public required string CveId { get; init; }
|
||||
|
||||
public required string SuppressReason { get; init; }
|
||||
|
||||
public required VexConsensusRef VexConsensus { get; init; }
|
||||
|
||||
public required WitnessEvidenceRef WitnessEvidence { get; init; }
|
||||
|
||||
public required string ReachabilityState { get; init; }
|
||||
|
||||
public required DateTimeOffset Timestamp { get; init; }
|
||||
|
||||
public DeterministicReplayInputs? DeterministicReplayInputs { get; init; }
|
||||
}
|
||||
|
||||
public sealed record VexConsensusRef
|
||||
{
|
||||
public required string Status { get; init; }
|
||||
|
||||
public string? Justification { get; init; }
|
||||
|
||||
public required double ConfidenceScore { get; init; }
|
||||
|
||||
public required string ConsensusDigest { get; init; }
|
||||
|
||||
public int SourceCount { get; init; }
|
||||
|
||||
public required DateTimeOffset ComputedAt { get; init; }
|
||||
}
|
||||
|
||||
public sealed record WitnessEvidenceRef
|
||||
{
|
||||
public required string WitnessId { get; init; }
|
||||
|
||||
public required string DsseDigest { get; init; }
|
||||
|
||||
public required string ObservationType { get; init; }
|
||||
|
||||
public required string PredicateType { get; init; }
|
||||
}
|
||||
|
||||
public sealed record DeterministicReplayInputs
|
||||
{
|
||||
public required string CanonicalId { get; init; }
|
||||
|
||||
public required string VexConsensusDigest { get; init; }
|
||||
|
||||
public required string WitnessId { get; init; }
|
||||
}
|
||||
Reference in New Issue
Block a user