Gaps fill up, fixes, ui restructuring

This commit is contained in:
master
2026-02-19 22:10:54 +02:00
parent b5829dce5c
commit 04cacdca8a
331 changed files with 42859 additions and 2174 deletions

View File

@@ -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);
}

View File

@@ -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; }
}

View File

@@ -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>

View File

@@ -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;

View File

@@ -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.';

View File

@@ -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;
}
}

View File

@@ -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; }
}

View File

@@ -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),
};
}
}

View File

@@ -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; }
}