wip: doctor/cli/docs/api to vector db consolidation; api hardening for descriptions, tenant, and scopes; migrations and conversions of all DALs to EF v10

This commit is contained in:
master
2026-02-23 15:30:50 +02:00
parent bd8fee6ed8
commit e746577380
1424 changed files with 81225 additions and 25251 deletions

View File

@@ -1,6 +1,8 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using StellaOps.Auth.ServerIntegration;
using StellaOps.Auth.ServerIntegration.Tenancy;
namespace StellaOps.EvidenceLocker.Api;
@@ -65,37 +67,44 @@ public static class EvidenceAuditEndpoints
public static void MapEvidenceAuditEndpoints(this WebApplication app)
{
var group = app.MapGroup("/api/v1/evidence")
.WithTags("Evidence Audit");
.WithTags("Evidence Audit")
.RequireTenant();
group.MapGet(string.Empty, GetHome)
.WithName("GetEvidenceHome")
.WithSummary("Get evidence home summary and quick links.")
.RequireAuthorization();
.WithDescription("Returns an evidence home dashboard summary including quick stats for the last 24 hours/7 days/30 days (new packs, sealed bundles, failed verifications, trust alerts) and lists of latest packs and failed verifications.")
.RequireAuthorization(StellaOpsResourceServerPolicies.EvidenceRead);
group.MapGet("/packs", ListPacks)
.WithName("ListEvidencePacks")
.WithSummary("List evidence packs.")
.RequireAuthorization();
.WithDescription("Lists all registered evidence packs ordered by pack ID, each with release ID, environment, bundle version, seal status, and creation timestamp.")
.RequireAuthorization(StellaOpsResourceServerPolicies.EvidenceRead);
group.MapGet("/packs/{id}", GetPackDetail)
.WithName("GetEvidencePack")
.WithSummary("Get evidence pack detail.")
.RequireAuthorization();
.WithDescription("Returns the full detail record for a specific evidence pack including all artifact references, promotion run ID, manifest digest, release decision, and proof chain ID. Returns 404 if the pack ID is not registered.")
.RequireAuthorization(StellaOpsResourceServerPolicies.EvidenceRead);
group.MapGet("/proofs/{subjectDigest}", GetProofChain)
.WithName("GetEvidenceProofChain")
.WithSummary("Get proof chain by subject digest.")
.RequireAuthorization();
.WithDescription("Returns the proof chain record for an evidence artifact identified by its subject digest, including DSSE envelope reference, Rekor entry URL, and verification timestamp. Returns 404 if no proof chain exists for the digest.")
.RequireAuthorization(StellaOpsResourceServerPolicies.EvidenceRead);
group.MapGet("/audit", ListAudit)
.WithName("ListEvidenceAuditLog")
.WithSummary("Get unified evidence audit log slice.")
.RequireAuthorization();
.WithDescription("Returns a paginated slice of the unified evidence audit log showing export, pack, and trust events. Use the limit query parameter (max 200) to control page size.")
.RequireAuthorization(StellaOpsResourceServerPolicies.EvidenceRead);
group.MapGet("/receipts/cvss/{id}", GetCvssReceipt)
.WithName("GetCvssReceipt")
.WithSummary("Get CVSS receipt by vulnerability id.")
.RequireAuthorization();
.WithDescription("Returns the CVSS scoring receipt for a specific vulnerability ID, including base score, CVSS vector, scoring timestamp, and source. Returns 404 if no receipt is on file for the vulnerability.")
.RequireAuthorization(StellaOpsResourceServerPolicies.EvidenceRead);
}
private static IResult GetHome()

View File

@@ -2,6 +2,8 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using StellaOps.Auth.ServerIntegration;
using StellaOps.Auth.ServerIntegration.Tenancy;
using StellaOps.EvidenceLocker.Storage;
using System.Text.Json;
@@ -22,12 +24,15 @@ public static class EvidenceThreadEndpoints
public static void MapEvidenceThreadEndpoints(this WebApplication app)
{
var group = app.MapGroup("/api/v1/evidence/thread")
.WithTags("Evidence Threads");
.WithTags("Evidence Threads")
.RequireTenant();
// GET /api/v1/evidence/thread/{canonicalId}
group.MapGet("/{canonicalId}", GetThreadByCanonicalIdAsync)
.WithName("GetEvidenceThread")
.WithSummary("Retrieve the evidence thread for an artifact by canonical_id")
.WithDescription("Returns the Artifact Canonical Record for an artifact identified by its canonical ID, including format, artifact digest, PURL, and associated DSSE attestations ordered by signing timestamp. Use include_attestations=false to suppress the attestation list.")
.RequireAuthorization(StellaOpsResourceServerPolicies.EvidenceRead)
.Produces<GetEvidenceThreadResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status500InternalServerError);
@@ -36,6 +41,8 @@ public static class EvidenceThreadEndpoints
group.MapGet("/", ListThreadsByPurlAsync)
.WithName("ListEvidenceThreads")
.WithSummary("List evidence threads matching a PURL")
.WithDescription("Lists all Artifact Canonical Records whose PURL matches the provided query parameter. Returns a paginated summary with per-thread attestation counts. The purl parameter is required.")
.RequireAuthorization(StellaOpsResourceServerPolicies.EvidenceRead)
.Produces<ListEvidenceThreadsResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status500InternalServerError);

View File

@@ -8,6 +8,8 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using StellaOps.Auth.ServerIntegration;
using StellaOps.Auth.ServerIntegration.Tenancy;
namespace StellaOps.EvidenceLocker.Api;
@@ -22,33 +24,37 @@ public static class ExportEndpoints
public static void MapExportEndpoints(this WebApplication app)
{
var group = app.MapGroup("/api/v1/bundles")
.WithTags("Export");
.WithTags("Export")
.RequireTenant();
// POST /api/v1/bundles/{bundleId}/export
group.MapPost("/{bundleId}/export", TriggerExportAsync)
.WithName("TriggerExport")
.WithSummary("Trigger an async evidence bundle export")
.WithDescription("Enqueues an asynchronous export job for a specific evidence bundle. Returns 202 Accepted with the export job ID and a status polling URL. Returns 404 if the bundle ID is not registered.")
.Produces<ExportTriggerResponse>(StatusCodes.Status202Accepted)
.Produces(StatusCodes.Status404NotFound)
.RequireAuthorization();
.RequireAuthorization(StellaOpsResourceServerPolicies.ExportOperator);
// GET /api/v1/bundles/{bundleId}/export/{exportId}
group.MapGet("/{bundleId}/export/{exportId}", GetExportStatusAsync)
.WithName("GetExportStatus")
.WithSummary("Get export status or download exported bundle")
.WithDescription("Returns the current status of an evidence bundle export job. Returns 200 with the export manifest when complete, or 202 if the export is still in progress. Returns 404 if the export ID is not found.")
.Produces<ExportStatusResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status202Accepted)
.Produces(StatusCodes.Status404NotFound)
.RequireAuthorization();
.RequireAuthorization(StellaOpsResourceServerPolicies.ExportViewer);
// GET /api/v1/bundles/{bundleId}/export/{exportId}/download
group.MapGet("/{bundleId}/export/{exportId}/download", DownloadExportAsync)
.WithName("DownloadExport")
.WithSummary("Download the exported bundle")
.WithDescription("Streams the completed evidence bundle as a gzip-compressed archive. Returns 409 Conflict if the export is still in progress. Returns 404 if the export ID is not found.")
.Produces(StatusCodes.Status200OK, contentType: "application/gzip")
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status409Conflict)
.RequireAuthorization();
.RequireAuthorization(StellaOpsResourceServerPolicies.ExportViewer);
}
private static async Task<IResult> TriggerExportAsync(

View File

@@ -3,6 +3,8 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using StellaOps.Auth.ServerIntegration;
using StellaOps.Auth.ServerIntegration.Tenancy;
using StellaOps.EvidenceLocker.Storage;
using System.Text.Json;
@@ -21,12 +23,15 @@ public static class VerdictEndpoints
public static void MapVerdictEndpoints(this WebApplication app)
{
var group = app.MapGroup("/api/v1/verdicts")
.WithTags("Verdicts");
.WithTags("Verdicts")
.RequireTenant();
// POST /api/v1/verdicts
group.MapPost("/", StoreVerdictAsync)
.WithName("StoreVerdict")
.WithSummary("Store a verdict attestation")
.WithDescription("Persists a verdict attestation record, including the policy run ID, policy ID, finding ID, decision, and DSSE envelope. Returns the stored verdict ID and creation timestamp. Requires write authorization.")
.RequireAuthorization(StellaOpsResourceServerPolicies.EvidenceCreate)
.Produces<StoreVerdictResponse>(StatusCodes.Status201Created)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status500InternalServerError);
@@ -35,6 +40,8 @@ public static class VerdictEndpoints
group.MapGet("/{verdictId}", GetVerdictAsync)
.WithName("GetVerdict")
.WithSummary("Retrieve a verdict attestation by ID")
.WithDescription("Returns the full verdict attestation record for the given verdict ID, including policy metadata, finding reference, decision, and the DSSE envelope. Returns 404 if the verdict ID is not found.")
.RequireAuthorization(StellaOpsResourceServerPolicies.EvidenceRead)
.Produces<GetVerdictResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status500InternalServerError);
@@ -44,6 +51,9 @@ public static class VerdictEndpoints
.WithName("ListVerdictsForRun")
.WithTags("Verdicts")
.WithSummary("List verdict attestations for a policy run")
.WithDescription("Lists all verdict attestations associated with a specific policy run ID. Returns a paginated collection ordered by creation time. Returns 404 if the run ID is not found.")
.RequireAuthorization(StellaOpsResourceServerPolicies.EvidenceRead)
.RequireTenant()
.Produces<ListVerdictsResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status500InternalServerError);
@@ -52,6 +62,8 @@ public static class VerdictEndpoints
group.MapPost("/{verdictId}/verify", VerifyVerdictAsync)
.WithName("VerifyVerdict")
.WithSummary("Verify verdict attestation signature")
.WithDescription("Verifies the DSSE envelope signature of a verdict attestation, confirming the signing key ID and signature integrity. Returns a structured result with isValid flag and per-step diagnostics. Returns 404 if the verdict ID is not found.")
.RequireAuthorization(StellaOpsResourceServerPolicies.EvidenceRead)
.Produces<VerifyVerdictResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status500InternalServerError);
@@ -60,6 +72,8 @@ public static class VerdictEndpoints
group.MapGet("/{verdictId}/envelope", DownloadEnvelopeAsync)
.WithName("DownloadEnvelope")
.WithSummary("Download DSSE envelope for verdict")
.WithDescription("Returns the raw DSSE envelope JSON for a specific verdict attestation. Used to retrieve the full envelope for offline verification or replay. Returns 404 if the verdict ID is not found.")
.RequireAuthorization(StellaOpsResourceServerPolicies.EvidenceRead)
.Produces(StatusCodes.Status200OK, contentType: "application/json")
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status500InternalServerError);

View File

@@ -8,17 +8,27 @@ Maintain Evidence Locker infrastructure services: storage backends, repositories
- Implement object-store adapters (filesystem and S3) with write-once semantics.
- Provide bundle packaging, portable bundle generation, and signature/timestamp workflows.
- Integrate timeline publishing and incident mode notifications.
- Maintain EF Core DbContext, entity models, and compiled model artifacts for the evidence_locker schema.
## DAL Technology
- Repositories use EF Core v10 (converted from Dapper/Npgsql as of Sprint 088).
- SQL migrations remain authoritative; EF models are scaffolded from the schema, never the reverse.
- No EF Core auto-migrations at runtime.
- Module is registered in Platform migration module registry as `EvidenceLocker`.
## Required Reading
- docs/modules/evidence-locker/architecture.md
- docs/modules/evidence-locker/bundle-packaging.md
- docs/modules/evidence-locker/attestation-contract.md
- docs/modules/platform/architecture-overview.md
- docs/db/EF_CORE_MODEL_GENERATION_STANDARDS.md
- docs/db/EF_CORE_RUNTIME_CUTOVER_STRATEGY.md
## Definition of Done
- Deterministic bundle packaging and portable output verified by tests.
- Migration runner applies scripts with checksum validation.
- Storage backends enforce write-once when configured.
- EF Core compiled model regenerated after any OnModelCreating changes.
## Working Agreement
- 1. Update task status to DOING/DONE in the sprint file and local TASKS.md.

View File

@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore;
using Npgsql;
using StellaOps.EvidenceLocker.Infrastructure.EfCore.CompiledModels;
using StellaOps.EvidenceLocker.Infrastructure.EfCore.Context;
namespace StellaOps.EvidenceLocker.Infrastructure.Db;
internal static class EvidenceLockerDbContextFactory
{
public const string DefaultSchemaName = "evidence_locker";
public static EvidenceLockerDbContext Create(NpgsqlConnection connection, int commandTimeoutSeconds, string schemaName)
{
var normalizedSchema = string.IsNullOrWhiteSpace(schemaName)
? DefaultSchemaName
: schemaName.Trim();
var optionsBuilder = new DbContextOptionsBuilder<EvidenceLockerDbContext>()
.UseNpgsql(connection, npgsql => npgsql.CommandTimeout(commandTimeoutSeconds));
if (string.Equals(normalizedSchema, DefaultSchemaName, StringComparison.Ordinal))
{
// Use the static compiled model when schema mapping matches the default model.
optionsBuilder.UseModel(EvidenceLockerDbContextModel.Instance);
}
return new EvidenceLockerDbContext(optionsBuilder.Options, normalizedSchema);
}
}

View File

@@ -0,0 +1,107 @@
// <auto-generated />
using System;
using System.Reflection;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using StellaOps.EvidenceLocker.Infrastructure.EfCore.Models;
#pragma warning disable 219, 612, 618
#nullable disable
namespace StellaOps.EvidenceLocker.Infrastructure.EfCore.CompiledModels
{
[EntityFrameworkInternal]
public partial class EvidenceArtifactEntityType
{
public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType baseEntityType = null)
{
var runtimeEntityType = model.AddEntityType(
"StellaOps.EvidenceLocker.Infrastructure.EfCore.Models.EvidenceArtifactEntity",
typeof(EvidenceArtifactEntity), baseEntityType,
propertyCount: 9, namedIndexCount: 2, keyCount: 1);
var artifactId = runtimeEntityType.AddProperty("ArtifactId", typeof(Guid),
propertyInfo: typeof(EvidenceArtifactEntity).GetProperty("ArtifactId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceArtifactEntity).GetField("<ArtifactId>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
afterSaveBehavior: PropertySaveBehavior.Throw, sentinel: Guid.Empty);
artifactId.AddAnnotation("Relational:ColumnName", "artifact_id");
var bundleId = runtimeEntityType.AddProperty("BundleId", typeof(Guid),
propertyInfo: typeof(EvidenceArtifactEntity).GetProperty("BundleId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceArtifactEntity).GetField("<BundleId>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
sentinel: Guid.Empty);
bundleId.AddAnnotation("Relational:ColumnName", "bundle_id");
var tenantId = runtimeEntityType.AddProperty("TenantId", typeof(Guid),
propertyInfo: typeof(EvidenceArtifactEntity).GetProperty("TenantId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceArtifactEntity).GetField("<TenantId>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
sentinel: Guid.Empty);
tenantId.AddAnnotation("Relational:ColumnName", "tenant_id");
var name = runtimeEntityType.AddProperty("Name", typeof(string),
propertyInfo: typeof(EvidenceArtifactEntity).GetProperty("Name", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceArtifactEntity).GetField("<Name>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
name.AddAnnotation("Relational:ColumnName", "name");
var contentType = runtimeEntityType.AddProperty("ContentType", typeof(string),
propertyInfo: typeof(EvidenceArtifactEntity).GetProperty("ContentType", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceArtifactEntity).GetField("<ContentType>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
contentType.AddAnnotation("Relational:ColumnName", "content_type");
var sizeBytes = runtimeEntityType.AddProperty("SizeBytes", typeof(long),
propertyInfo: typeof(EvidenceArtifactEntity).GetProperty("SizeBytes", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceArtifactEntity).GetField("<SizeBytes>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
sentinel: 0L);
sizeBytes.AddAnnotation("Relational:ColumnName", "size_bytes");
var storageKey = runtimeEntityType.AddProperty("StorageKey", typeof(string),
propertyInfo: typeof(EvidenceArtifactEntity).GetProperty("StorageKey", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceArtifactEntity).GetField("<StorageKey>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
storageKey.AddAnnotation("Relational:ColumnName", "storage_key");
var sha256 = runtimeEntityType.AddProperty("Sha256", typeof(string),
propertyInfo: typeof(EvidenceArtifactEntity).GetProperty("Sha256", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceArtifactEntity).GetField("<Sha256>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
sha256.AddAnnotation("Relational:ColumnName", "sha256");
var createdAt = runtimeEntityType.AddProperty("CreatedAt", typeof(DateTime),
propertyInfo: typeof(EvidenceArtifactEntity).GetProperty("CreatedAt", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceArtifactEntity).GetField("<CreatedAt>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
valueGenerated: ValueGenerated.OnAdd, sentinel: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
createdAt.AddAnnotation("Relational:ColumnName", "created_at");
createdAt.AddAnnotation("Relational:DefaultValueSql", "(NOW() AT TIME ZONE 'UTC')");
var key = runtimeEntityType.AddKey(new[] { artifactId });
runtimeEntityType.SetPrimaryKey(key);
key.AddAnnotation("Relational:Name", "evidence_artifacts_pkey");
runtimeEntityType.AddIndex(new[] { bundleId }, name: "ix_evidence_artifacts_bundle_id");
runtimeEntityType.AddIndex(new[] { tenantId, storageKey }, name: "uq_evidence_artifacts_storage_key", unique: true);
return runtimeEntityType;
}
public static void CreateAnnotations(RuntimeEntityType runtimeEntityType)
{
runtimeEntityType.AddAnnotation("Relational:Schema", "evidence_locker");
runtimeEntityType.AddAnnotation("Relational:TableName", "evidence_artifacts");
Customize(runtimeEntityType);
}
public static void CreateForeignKey1(RuntimeEntityType declaringEntityType, RuntimeEntityType principalEntityType)
{
var bundleIdProperty = declaringEntityType.FindProperty("BundleId");
var fk = declaringEntityType.AddForeignKey(
new[] { bundleIdProperty },
principalEntityType.FindKey(new[] { principalEntityType.FindProperty("BundleId") }),
principalEntityType, deleteBehavior: DeleteBehavior.Cascade, required: true);
fk.AddAnnotation("Relational:Name", "fk_artifacts_bundle");
}
public static void CreateNavigations(RuntimeEntityType runtimeEntityType) { }
static partial void Customize(RuntimeEntityType runtimeEntityType);
}
}

View File

@@ -0,0 +1,187 @@
// <auto-generated />
using System;
using System.Collections.Generic;
using System.Reflection;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using StellaOps.EvidenceLocker.Infrastructure.EfCore.Models;
#pragma warning disable 219, 612, 618
#nullable disable
namespace StellaOps.EvidenceLocker.Infrastructure.EfCore.CompiledModels
{
[EntityFrameworkInternal]
public partial class EvidenceBundleEntityType
{
public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType baseEntityType = null)
{
var runtimeEntityType = model.AddEntityType(
"StellaOps.EvidenceLocker.Infrastructure.EfCore.Models.EvidenceBundleEntity",
typeof(EvidenceBundleEntity),
baseEntityType,
propertyCount: 13,
namedIndexCount: 2,
keyCount: 1);
var bundleId = runtimeEntityType.AddProperty(
"BundleId",
typeof(Guid),
propertyInfo: typeof(EvidenceBundleEntity).GetProperty("BundleId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceBundleEntity).GetField("<BundleId>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
afterSaveBehavior: PropertySaveBehavior.Throw,
sentinel: new Guid("00000000-0000-0000-0000-000000000000"));
bundleId.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
bundleId.AddAnnotation("Relational:ColumnName", "bundle_id");
var tenantId = runtimeEntityType.AddProperty(
"TenantId",
typeof(Guid),
propertyInfo: typeof(EvidenceBundleEntity).GetProperty("TenantId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceBundleEntity).GetField("<TenantId>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
sentinel: new Guid("00000000-0000-0000-0000-000000000000"));
tenantId.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
tenantId.AddAnnotation("Relational:ColumnName", "tenant_id");
var kind = runtimeEntityType.AddProperty(
"Kind",
typeof(short),
propertyInfo: typeof(EvidenceBundleEntity).GetProperty("Kind", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceBundleEntity).GetField("<Kind>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
sentinel: (short)0);
kind.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
kind.AddAnnotation("Relational:ColumnName", "kind");
var status = runtimeEntityType.AddProperty(
"Status",
typeof(short),
propertyInfo: typeof(EvidenceBundleEntity).GetProperty("Status", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceBundleEntity).GetField("<Status>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
sentinel: (short)0);
status.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
status.AddAnnotation("Relational:ColumnName", "status");
var rootHash = runtimeEntityType.AddProperty(
"RootHash",
typeof(string),
propertyInfo: typeof(EvidenceBundleEntity).GetProperty("RootHash", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceBundleEntity).GetField("<RootHash>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
rootHash.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
rootHash.AddAnnotation("Relational:ColumnName", "root_hash");
var storageKey = runtimeEntityType.AddProperty(
"StorageKey",
typeof(string),
propertyInfo: typeof(EvidenceBundleEntity).GetProperty("StorageKey", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceBundleEntity).GetField("<StorageKey>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
storageKey.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
storageKey.AddAnnotation("Relational:ColumnName", "storage_key");
var description = runtimeEntityType.AddProperty(
"Description",
typeof(string),
propertyInfo: typeof(EvidenceBundleEntity).GetProperty("Description", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceBundleEntity).GetField("<Description>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
nullable: true);
description.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
description.AddAnnotation("Relational:ColumnName", "description");
var sealedAt = runtimeEntityType.AddProperty(
"SealedAt",
typeof(DateTime?),
propertyInfo: typeof(EvidenceBundleEntity).GetProperty("SealedAt", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceBundleEntity).GetField("<SealedAt>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
nullable: true);
sealedAt.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
sealedAt.AddAnnotation("Relational:ColumnName", "sealed_at");
var expiresAt = runtimeEntityType.AddProperty(
"ExpiresAt",
typeof(DateTime?),
propertyInfo: typeof(EvidenceBundleEntity).GetProperty("ExpiresAt", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceBundleEntity).GetField("<ExpiresAt>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
nullable: true);
expiresAt.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
expiresAt.AddAnnotation("Relational:ColumnName", "expires_at");
var createdAt = runtimeEntityType.AddProperty(
"CreatedAt",
typeof(DateTime),
propertyInfo: typeof(EvidenceBundleEntity).GetProperty("CreatedAt", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceBundleEntity).GetField("<CreatedAt>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
valueGenerated: ValueGenerated.OnAdd,
sentinel: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
createdAt.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
createdAt.AddAnnotation("Relational:ColumnName", "created_at");
createdAt.AddAnnotation("Relational:DefaultValueSql", "(NOW() AT TIME ZONE 'UTC')");
var updatedAt = runtimeEntityType.AddProperty(
"UpdatedAt",
typeof(DateTime),
propertyInfo: typeof(EvidenceBundleEntity).GetProperty("UpdatedAt", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceBundleEntity).GetField("<UpdatedAt>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
valueGenerated: ValueGenerated.OnAdd,
sentinel: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
updatedAt.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
updatedAt.AddAnnotation("Relational:ColumnName", "updated_at");
updatedAt.AddAnnotation("Relational:DefaultValueSql", "(NOW() AT TIME ZONE 'UTC')");
var portableStorageKey = runtimeEntityType.AddProperty(
"PortableStorageKey",
typeof(string),
propertyInfo: typeof(EvidenceBundleEntity).GetProperty("PortableStorageKey", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceBundleEntity).GetField("<PortableStorageKey>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
nullable: true);
portableStorageKey.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
portableStorageKey.AddAnnotation("Relational:ColumnName", "portable_storage_key");
var portableGeneratedAt = runtimeEntityType.AddProperty(
"PortableGeneratedAt",
typeof(DateTime?),
propertyInfo: typeof(EvidenceBundleEntity).GetProperty("PortableGeneratedAt", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceBundleEntity).GetField("<PortableGeneratedAt>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
nullable: true);
portableGeneratedAt.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
portableGeneratedAt.AddAnnotation("Relational:ColumnName", "portable_generated_at");
var key = runtimeEntityType.AddKey(
new[] { bundleId });
runtimeEntityType.SetPrimaryKey(key);
key.AddAnnotation("Relational:Name", "evidence_bundles_pkey");
var uq_storage_key = runtimeEntityType.AddIndex(
new[] { tenantId, storageKey },
name: "uq_evidence_bundles_storage_key",
unique: true);
var uq_portable_storage_key = runtimeEntityType.AddIndex(
new[] { tenantId, portableStorageKey },
name: "uq_evidence_bundles_portable_storage_key",
unique: true);
uq_portable_storage_key.AddAnnotation("Relational:Filter", "portable_storage_key IS NOT NULL");
return runtimeEntityType;
}
public static void CreateAnnotations(RuntimeEntityType runtimeEntityType)
{
runtimeEntityType.AddAnnotation("Relational:FunctionName", null);
runtimeEntityType.AddAnnotation("Relational:Schema", "evidence_locker");
runtimeEntityType.AddAnnotation("Relational:SqlQuery", null);
runtimeEntityType.AddAnnotation("Relational:TableName", "evidence_bundles");
runtimeEntityType.AddAnnotation("Relational:ViewName", null);
runtimeEntityType.AddAnnotation("Relational:ViewSchema", null);
Customize(runtimeEntityType);
}
public static void CreateNavigations(RuntimeEntityType runtimeEntityType)
{
// Navigations configured by relationship entity types
}
static partial void Customize(RuntimeEntityType runtimeEntityType);
}
}

View File

@@ -0,0 +1,137 @@
// <auto-generated />
using System;
using System.Reflection;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using StellaOps.EvidenceLocker.Infrastructure.EfCore.Models;
#pragma warning disable 219, 612, 618
#nullable disable
namespace StellaOps.EvidenceLocker.Infrastructure.EfCore.CompiledModels
{
[EntityFrameworkInternal]
public partial class EvidenceBundleSignatureEntityType
{
public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType baseEntityType = null)
{
var runtimeEntityType = model.AddEntityType(
"StellaOps.EvidenceLocker.Infrastructure.EfCore.Models.EvidenceBundleSignatureEntity",
typeof(EvidenceBundleSignatureEntity),
baseEntityType,
propertyCount: 12,
namedIndexCount: 1,
keyCount: 1);
var bundleId = runtimeEntityType.AddProperty(
"BundleId", typeof(Guid),
propertyInfo: typeof(EvidenceBundleSignatureEntity).GetProperty("BundleId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceBundleSignatureEntity).GetField("<BundleId>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
afterSaveBehavior: PropertySaveBehavior.Throw,
sentinel: Guid.Empty);
bundleId.AddAnnotation("Relational:ColumnName", "bundle_id");
var tenantId = runtimeEntityType.AddProperty(
"TenantId", typeof(Guid),
propertyInfo: typeof(EvidenceBundleSignatureEntity).GetProperty("TenantId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceBundleSignatureEntity).GetField("<TenantId>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
afterSaveBehavior: PropertySaveBehavior.Throw,
sentinel: Guid.Empty);
tenantId.AddAnnotation("Relational:ColumnName", "tenant_id");
var payloadType = runtimeEntityType.AddProperty("PayloadType", typeof(string),
propertyInfo: typeof(EvidenceBundleSignatureEntity).GetProperty("PayloadType", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceBundleSignatureEntity).GetField("<PayloadType>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
payloadType.AddAnnotation("Relational:ColumnName", "payload_type");
var payload = runtimeEntityType.AddProperty("Payload", typeof(string),
propertyInfo: typeof(EvidenceBundleSignatureEntity).GetProperty("Payload", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceBundleSignatureEntity).GetField("<Payload>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
payload.AddAnnotation("Relational:ColumnName", "payload");
var signature = runtimeEntityType.AddProperty("Signature", typeof(string),
propertyInfo: typeof(EvidenceBundleSignatureEntity).GetProperty("Signature", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceBundleSignatureEntity).GetField("<Signature>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
signature.AddAnnotation("Relational:ColumnName", "signature");
var keyId = runtimeEntityType.AddProperty("KeyId", typeof(string),
propertyInfo: typeof(EvidenceBundleSignatureEntity).GetProperty("KeyId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceBundleSignatureEntity).GetField("<KeyId>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
nullable: true);
keyId.AddAnnotation("Relational:ColumnName", "key_id");
var algorithm = runtimeEntityType.AddProperty("Algorithm", typeof(string),
propertyInfo: typeof(EvidenceBundleSignatureEntity).GetProperty("Algorithm", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceBundleSignatureEntity).GetField("<Algorithm>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
algorithm.AddAnnotation("Relational:ColumnName", "algorithm");
var provider = runtimeEntityType.AddProperty("Provider", typeof(string),
propertyInfo: typeof(EvidenceBundleSignatureEntity).GetProperty("Provider", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceBundleSignatureEntity).GetField("<Provider>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
provider.AddAnnotation("Relational:ColumnName", "provider");
var signedAt = runtimeEntityType.AddProperty("SignedAt", typeof(DateTime),
propertyInfo: typeof(EvidenceBundleSignatureEntity).GetProperty("SignedAt", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceBundleSignatureEntity).GetField("<SignedAt>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
sentinel: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
signedAt.AddAnnotation("Relational:ColumnName", "signed_at");
var timestampedAt = runtimeEntityType.AddProperty("TimestampedAt", typeof(DateTime?),
propertyInfo: typeof(EvidenceBundleSignatureEntity).GetProperty("TimestampedAt", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceBundleSignatureEntity).GetField("<TimestampedAt>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
nullable: true);
timestampedAt.AddAnnotation("Relational:ColumnName", "timestamped_at");
var timestampAuthority = runtimeEntityType.AddProperty("TimestampAuthority", typeof(string),
propertyInfo: typeof(EvidenceBundleSignatureEntity).GetProperty("TimestampAuthority", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceBundleSignatureEntity).GetField("<TimestampAuthority>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
nullable: true);
timestampAuthority.AddAnnotation("Relational:ColumnName", "timestamp_authority");
var timestampToken = runtimeEntityType.AddProperty("TimestampToken", typeof(byte[]),
propertyInfo: typeof(EvidenceBundleSignatureEntity).GetProperty("TimestampToken", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceBundleSignatureEntity).GetField("<TimestampToken>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
nullable: true);
timestampToken.AddAnnotation("Relational:ColumnName", "timestamp_token");
var key = runtimeEntityType.AddKey(new[] { bundleId, tenantId });
runtimeEntityType.SetPrimaryKey(key);
key.AddAnnotation("Relational:Name", "evidence_bundle_signatures_pkey");
var ix_signed_at = runtimeEntityType.AddIndex(
new[] { tenantId, signedAt },
name: "ix_evidence_bundle_signatures_signed_at");
ix_signed_at.AddAnnotation("Relational:IsDescending", new[] { false, true });
return runtimeEntityType;
}
public static void CreateAnnotations(RuntimeEntityType runtimeEntityType)
{
runtimeEntityType.AddAnnotation("Relational:Schema", "evidence_locker");
runtimeEntityType.AddAnnotation("Relational:TableName", "evidence_bundle_signatures");
Customize(runtimeEntityType);
}
public static void CreateForeignKey1(RuntimeEntityType declaringEntityType, RuntimeEntityType principalEntityType)
{
var bundleIdProperty = declaringEntityType.FindProperty("BundleId");
var fk = declaringEntityType.AddForeignKey(
new[] { bundleIdProperty },
principalEntityType.FindKey(new[] { principalEntityType.FindProperty("BundleId") }),
principalEntityType,
deleteBehavior: DeleteBehavior.Cascade,
unique: true,
required: true);
fk.AddAnnotation("Relational:Name", "fk_evidence_bundle_signatures_bundle");
}
public static void CreateNavigations(RuntimeEntityType runtimeEntityType)
{
}
static partial void Customize(RuntimeEntityType runtimeEntityType);
}
}

View File

@@ -0,0 +1,132 @@
// <auto-generated />
using System;
using System.Reflection;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using StellaOps.EvidenceLocker.Infrastructure.EfCore.Models;
#pragma warning disable 219, 612, 618
#nullable disable
namespace StellaOps.EvidenceLocker.Infrastructure.EfCore.CompiledModels
{
[EntityFrameworkInternal]
public partial class EvidenceGateArtifactEntityType
{
public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType baseEntityType = null)
{
var runtimeEntityType = model.AddEntityType(
"StellaOps.EvidenceLocker.Infrastructure.EfCore.Models.EvidenceGateArtifactEntity",
typeof(EvidenceGateArtifactEntity), baseEntityType,
propertyCount: 15, namedIndexCount: 2, keyCount: 1);
var tenantId = runtimeEntityType.AddProperty("TenantId", typeof(Guid),
propertyInfo: typeof(EvidenceGateArtifactEntity).GetProperty("TenantId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceGateArtifactEntity).GetField("<TenantId>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
afterSaveBehavior: PropertySaveBehavior.Throw, sentinel: Guid.Empty);
tenantId.AddAnnotation("Relational:ColumnName", "tenant_id");
var artifactId = runtimeEntityType.AddProperty("ArtifactId", typeof(string),
propertyInfo: typeof(EvidenceGateArtifactEntity).GetProperty("ArtifactId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceGateArtifactEntity).GetField("<ArtifactId>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
afterSaveBehavior: PropertySaveBehavior.Throw);
artifactId.AddAnnotation("Relational:ColumnName", "artifact_id");
var evidenceId = runtimeEntityType.AddProperty("EvidenceId", typeof(string),
propertyInfo: typeof(EvidenceGateArtifactEntity).GetProperty("EvidenceId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceGateArtifactEntity).GetField("<EvidenceId>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
evidenceId.AddAnnotation("Relational:ColumnName", "evidence_id");
var canonicalBomSha256 = runtimeEntityType.AddProperty("CanonicalBomSha256", typeof(string),
propertyInfo: typeof(EvidenceGateArtifactEntity).GetProperty("CanonicalBomSha256", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceGateArtifactEntity).GetField("<CanonicalBomSha256>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
canonicalBomSha256.AddAnnotation("Relational:ColumnName", "canonical_bom_sha256");
var payloadDigest = runtimeEntityType.AddProperty("PayloadDigest", typeof(string),
propertyInfo: typeof(EvidenceGateArtifactEntity).GetProperty("PayloadDigest", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceGateArtifactEntity).GetField("<PayloadDigest>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
payloadDigest.AddAnnotation("Relational:ColumnName", "payload_digest");
var dsseEnvelopeRef = runtimeEntityType.AddProperty("DsseEnvelopeRef", typeof(string),
propertyInfo: typeof(EvidenceGateArtifactEntity).GetProperty("DsseEnvelopeRef", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceGateArtifactEntity).GetField("<DsseEnvelopeRef>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
dsseEnvelopeRef.AddAnnotation("Relational:ColumnName", "dsse_envelope_ref");
var rekorIndex = runtimeEntityType.AddProperty("RekorIndex", typeof(long),
propertyInfo: typeof(EvidenceGateArtifactEntity).GetProperty("RekorIndex", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceGateArtifactEntity).GetField("<RekorIndex>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
sentinel: 0L);
rekorIndex.AddAnnotation("Relational:ColumnName", "rekor_index");
var rekorTileId = runtimeEntityType.AddProperty("RekorTileId", typeof(string),
propertyInfo: typeof(EvidenceGateArtifactEntity).GetProperty("RekorTileId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceGateArtifactEntity).GetField("<RekorTileId>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
rekorTileId.AddAnnotation("Relational:ColumnName", "rekor_tile_id");
var rekorInclusionProofRef = runtimeEntityType.AddProperty("RekorInclusionProofRef", typeof(string),
propertyInfo: typeof(EvidenceGateArtifactEntity).GetProperty("RekorInclusionProofRef", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceGateArtifactEntity).GetField("<RekorInclusionProofRef>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
rekorInclusionProofRef.AddAnnotation("Relational:ColumnName", "rekor_inclusion_proof_ref");
var attestationRefs = runtimeEntityType.AddProperty("AttestationRefs", typeof(string),
propertyInfo: typeof(EvidenceGateArtifactEntity).GetProperty("AttestationRefs", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceGateArtifactEntity).GetField("<AttestationRefs>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
valueGenerated: ValueGenerated.OnAdd);
attestationRefs.AddAnnotation("Relational:ColumnName", "attestation_refs");
attestationRefs.AddAnnotation("Relational:ColumnType", "jsonb");
attestationRefs.AddAnnotation("Relational:DefaultValueSql", "'[]'::jsonb");
var rawBomRef = runtimeEntityType.AddProperty("RawBomRef", typeof(string),
propertyInfo: typeof(EvidenceGateArtifactEntity).GetProperty("RawBomRef", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceGateArtifactEntity).GetField("<RawBomRef>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
nullable: true);
rawBomRef.AddAnnotation("Relational:ColumnName", "raw_bom_ref");
var vexRefs = runtimeEntityType.AddProperty("VexRefs", typeof(string),
propertyInfo: typeof(EvidenceGateArtifactEntity).GetProperty("VexRefs", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceGateArtifactEntity).GetField("<VexRefs>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
valueGenerated: ValueGenerated.OnAdd);
vexRefs.AddAnnotation("Relational:ColumnName", "vex_refs");
vexRefs.AddAnnotation("Relational:ColumnType", "jsonb");
vexRefs.AddAnnotation("Relational:DefaultValueSql", "'[]'::jsonb");
var evidenceScore = runtimeEntityType.AddProperty("EvidenceScore", typeof(string),
propertyInfo: typeof(EvidenceGateArtifactEntity).GetProperty("EvidenceScore", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceGateArtifactEntity).GetField("<EvidenceScore>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
evidenceScore.AddAnnotation("Relational:ColumnName", "evidence_score");
var createdAt = runtimeEntityType.AddProperty("CreatedAt", typeof(DateTime),
propertyInfo: typeof(EvidenceGateArtifactEntity).GetProperty("CreatedAt", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceGateArtifactEntity).GetField("<CreatedAt>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
valueGenerated: ValueGenerated.OnAdd, sentinel: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
createdAt.AddAnnotation("Relational:ColumnName", "created_at");
createdAt.AddAnnotation("Relational:DefaultValueSql", "(NOW() AT TIME ZONE 'UTC')");
var updatedAt = runtimeEntityType.AddProperty("UpdatedAt", typeof(DateTime),
propertyInfo: typeof(EvidenceGateArtifactEntity).GetProperty("UpdatedAt", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceGateArtifactEntity).GetField("<UpdatedAt>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
valueGenerated: ValueGenerated.OnAdd, sentinel: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
updatedAt.AddAnnotation("Relational:ColumnName", "updated_at");
updatedAt.AddAnnotation("Relational:DefaultValueSql", "(NOW() AT TIME ZONE 'UTC')");
var key = runtimeEntityType.AddKey(new[] { tenantId, artifactId });
runtimeEntityType.SetPrimaryKey(key);
key.AddAnnotation("Relational:Name", "pk_evidence_gate_artifacts");
runtimeEntityType.AddIndex(new[] { evidenceId }, name: "uq_evidence_gate_artifacts_evidence_id", unique: true);
runtimeEntityType.AddIndex(new[] { tenantId, evidenceScore }, name: "ix_evidence_gate_artifacts_tenant_score");
return runtimeEntityType;
}
public static void CreateAnnotations(RuntimeEntityType runtimeEntityType)
{
runtimeEntityType.AddAnnotation("Relational:Schema", "evidence_locker");
runtimeEntityType.AddAnnotation("Relational:TableName", "evidence_gate_artifacts");
Customize(runtimeEntityType);
}
static partial void Customize(RuntimeEntityType runtimeEntityType);
}
}

View File

@@ -0,0 +1,108 @@
// <auto-generated />
using System;
using System.Reflection;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using StellaOps.EvidenceLocker.Infrastructure.EfCore.Models;
#pragma warning disable 219, 612, 618
#nullable disable
namespace StellaOps.EvidenceLocker.Infrastructure.EfCore.CompiledModels
{
[EntityFrameworkInternal]
public partial class EvidenceHoldEntityType
{
public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType baseEntityType = null)
{
var runtimeEntityType = model.AddEntityType(
"StellaOps.EvidenceLocker.Infrastructure.EfCore.Models.EvidenceHoldEntity",
typeof(EvidenceHoldEntity), baseEntityType,
propertyCount: 9, namedIndexCount: 1, keyCount: 1);
var holdId = runtimeEntityType.AddProperty("HoldId", typeof(Guid),
propertyInfo: typeof(EvidenceHoldEntity).GetProperty("HoldId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceHoldEntity).GetField("<HoldId>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
afterSaveBehavior: PropertySaveBehavior.Throw, sentinel: Guid.Empty);
holdId.AddAnnotation("Relational:ColumnName", "hold_id");
var tenantId = runtimeEntityType.AddProperty("TenantId", typeof(Guid),
propertyInfo: typeof(EvidenceHoldEntity).GetProperty("TenantId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceHoldEntity).GetField("<TenantId>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
sentinel: Guid.Empty);
tenantId.AddAnnotation("Relational:ColumnName", "tenant_id");
var bundleId = runtimeEntityType.AddProperty("BundleId", typeof(Guid?),
propertyInfo: typeof(EvidenceHoldEntity).GetProperty("BundleId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceHoldEntity).GetField("<BundleId>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
nullable: true);
bundleId.AddAnnotation("Relational:ColumnName", "bundle_id");
var caseId = runtimeEntityType.AddProperty("CaseId", typeof(string),
propertyInfo: typeof(EvidenceHoldEntity).GetProperty("CaseId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceHoldEntity).GetField("<CaseId>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
caseId.AddAnnotation("Relational:ColumnName", "case_id");
var reason = runtimeEntityType.AddProperty("Reason", typeof(string),
propertyInfo: typeof(EvidenceHoldEntity).GetProperty("Reason", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceHoldEntity).GetField("<Reason>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
reason.AddAnnotation("Relational:ColumnName", "reason");
var notes = runtimeEntityType.AddProperty("Notes", typeof(string),
propertyInfo: typeof(EvidenceHoldEntity).GetProperty("Notes", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceHoldEntity).GetField("<Notes>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
nullable: true);
notes.AddAnnotation("Relational:ColumnName", "notes");
var createdAt = runtimeEntityType.AddProperty("CreatedAt", typeof(DateTime),
propertyInfo: typeof(EvidenceHoldEntity).GetProperty("CreatedAt", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceHoldEntity).GetField("<CreatedAt>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
valueGenerated: ValueGenerated.OnAdd, sentinel: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
createdAt.AddAnnotation("Relational:ColumnName", "created_at");
createdAt.AddAnnotation("Relational:DefaultValueSql", "(NOW() AT TIME ZONE 'UTC')");
var expiresAt = runtimeEntityType.AddProperty("ExpiresAt", typeof(DateTime?),
propertyInfo: typeof(EvidenceHoldEntity).GetProperty("ExpiresAt", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceHoldEntity).GetField("<ExpiresAt>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
nullable: true);
expiresAt.AddAnnotation("Relational:ColumnName", "expires_at");
var releasedAt = runtimeEntityType.AddProperty("ReleasedAt", typeof(DateTime?),
propertyInfo: typeof(EvidenceHoldEntity).GetProperty("ReleasedAt", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(EvidenceHoldEntity).GetField("<ReleasedAt>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
nullable: true);
releasedAt.AddAnnotation("Relational:ColumnName", "released_at");
var key = runtimeEntityType.AddKey(new[] { holdId });
runtimeEntityType.SetPrimaryKey(key);
key.AddAnnotation("Relational:Name", "evidence_holds_pkey");
runtimeEntityType.AddIndex(new[] { tenantId, caseId }, name: "uq_evidence_holds_case", unique: true);
return runtimeEntityType;
}
public static void CreateAnnotations(RuntimeEntityType runtimeEntityType)
{
runtimeEntityType.AddAnnotation("Relational:Schema", "evidence_locker");
runtimeEntityType.AddAnnotation("Relational:TableName", "evidence_holds");
Customize(runtimeEntityType);
}
public static void CreateForeignKey1(RuntimeEntityType declaringEntityType, RuntimeEntityType principalEntityType)
{
var bundleIdProperty = declaringEntityType.FindProperty("BundleId");
var fk = declaringEntityType.AddForeignKey(
new[] { bundleIdProperty },
principalEntityType.FindKey(new[] { principalEntityType.FindProperty("BundleId") }),
principalEntityType, deleteBehavior: DeleteBehavior.SetNull, required: false);
fk.AddAnnotation("Relational:Name", "fk_evidence_holds_bundle");
}
public static void CreateNavigations(RuntimeEntityType runtimeEntityType) { }
static partial void Customize(RuntimeEntityType runtimeEntityType);
}
}

View File

@@ -0,0 +1,9 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore.Infrastructure;
using StellaOps.EvidenceLocker.Infrastructure.EfCore.CompiledModels;
using StellaOps.EvidenceLocker.Infrastructure.EfCore.Context;
#pragma warning disable 219, 612, 618
#nullable disable
[assembly: DbContextModel(typeof(EvidenceLockerDbContext), typeof(EvidenceLockerDbContextModel))]

View File

@@ -0,0 +1,48 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using StellaOps.EvidenceLocker.Infrastructure.EfCore.Context;
#pragma warning disable 219, 612, 618
#nullable disable
namespace StellaOps.EvidenceLocker.Infrastructure.EfCore.CompiledModels
{
[DbContext(typeof(EvidenceLockerDbContext))]
public partial class EvidenceLockerDbContextModel : RuntimeModel
{
private static readonly bool _useOldBehavior31751 =
System.AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue31751", out var enabled31751) && enabled31751;
static EvidenceLockerDbContextModel()
{
var model = new EvidenceLockerDbContextModel();
if (_useOldBehavior31751)
{
model.Initialize();
}
else
{
var thread = new System.Threading.Thread(RunInitialization, 10 * 1024 * 1024);
thread.Start();
thread.Join();
void RunInitialization()
{
model.Initialize();
}
}
model.Customize();
_instance = (EvidenceLockerDbContextModel)model.FinalizeModel();
}
private static EvidenceLockerDbContextModel _instance;
public static IModel Instance => _instance;
partial void Initialize();
partial void Customize();
}
}

View File

@@ -0,0 +1,49 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#pragma warning disable 219, 612, 618
#nullable disable
namespace StellaOps.EvidenceLocker.Infrastructure.EfCore.CompiledModels
{
public partial class EvidenceLockerDbContextModel
{
private EvidenceLockerDbContextModel()
: base(skipDetectChanges: false, modelId: new Guid("a1b2c3d4-e5f6-4789-abcd-ef0123456789"), entityTypeCount: 6)
{
}
partial void Initialize()
{
var evidenceBundleEntity = EvidenceBundleEntityType.Create(this);
var evidenceBundleSignatureEntity = EvidenceBundleSignatureEntityType.Create(this);
var evidenceArtifactEntity = EvidenceArtifactEntityType.Create(this);
var evidenceHoldEntity = EvidenceHoldEntityType.Create(this);
var evidenceGateArtifactEntity = EvidenceGateArtifactEntityType.Create(this);
var verdictAttestationEntity = VerdictAttestationEntityType.Create(this);
EvidenceBundleEntityType.CreateAnnotations(evidenceBundleEntity);
EvidenceBundleSignatureEntityType.CreateAnnotations(evidenceBundleSignatureEntity);
EvidenceArtifactEntityType.CreateAnnotations(evidenceArtifactEntity);
EvidenceHoldEntityType.CreateAnnotations(evidenceHoldEntity);
EvidenceGateArtifactEntityType.CreateAnnotations(evidenceGateArtifactEntity);
VerdictAttestationEntityType.CreateAnnotations(verdictAttestationEntity);
EvidenceBundleSignatureEntityType.CreateForeignKey1(evidenceBundleSignatureEntity, evidenceBundleEntity);
EvidenceArtifactEntityType.CreateForeignKey1(evidenceArtifactEntity, evidenceBundleEntity);
EvidenceHoldEntityType.CreateForeignKey1(evidenceHoldEntity, evidenceBundleEntity);
EvidenceBundleEntityType.CreateNavigations(evidenceBundleEntity);
EvidenceBundleSignatureEntityType.CreateNavigations(evidenceBundleSignatureEntity);
EvidenceArtifactEntityType.CreateNavigations(evidenceArtifactEntity);
EvidenceHoldEntityType.CreateNavigations(evidenceHoldEntity);
AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
AddAnnotation("ProductVersion", "10.0.0");
AddAnnotation("Relational:MaxIdentifierLength", 63);
}
}
}

View File

@@ -0,0 +1,139 @@
// <auto-generated />
using System;
using System.Reflection;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using StellaOps.EvidenceLocker.Infrastructure.EfCore.Models;
#pragma warning disable 219, 612, 618
#nullable disable
namespace StellaOps.EvidenceLocker.Infrastructure.EfCore.CompiledModels
{
[EntityFrameworkInternal]
public partial class VerdictAttestationEntityType
{
public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType baseEntityType = null)
{
var runtimeEntityType = model.AddEntityType(
"StellaOps.EvidenceLocker.Infrastructure.EfCore.Models.VerdictAttestationEntity",
typeof(VerdictAttestationEntity), baseEntityType,
propertyCount: 16, namedIndexCount: 6, keyCount: 1);
var verdictId = runtimeEntityType.AddProperty("VerdictId", typeof(string),
propertyInfo: typeof(VerdictAttestationEntity).GetProperty("VerdictId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(VerdictAttestationEntity).GetField("<VerdictId>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
afterSaveBehavior: PropertySaveBehavior.Throw);
verdictId.AddAnnotation("Relational:ColumnName", "verdict_id");
var tenantId = runtimeEntityType.AddProperty("TenantId", typeof(string),
propertyInfo: typeof(VerdictAttestationEntity).GetProperty("TenantId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(VerdictAttestationEntity).GetField("<TenantId>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
tenantId.AddAnnotation("Relational:ColumnName", "tenant_id");
var runId = runtimeEntityType.AddProperty("RunId", typeof(string),
propertyInfo: typeof(VerdictAttestationEntity).GetProperty("RunId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(VerdictAttestationEntity).GetField("<RunId>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
runId.AddAnnotation("Relational:ColumnName", "run_id");
var policyId = runtimeEntityType.AddProperty("PolicyId", typeof(string),
propertyInfo: typeof(VerdictAttestationEntity).GetProperty("PolicyId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(VerdictAttestationEntity).GetField("<PolicyId>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
policyId.AddAnnotation("Relational:ColumnName", "policy_id");
var policyVersion = runtimeEntityType.AddProperty("PolicyVersion", typeof(int),
propertyInfo: typeof(VerdictAttestationEntity).GetProperty("PolicyVersion", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(VerdictAttestationEntity).GetField("<PolicyVersion>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
sentinel: 0);
policyVersion.AddAnnotation("Relational:ColumnName", "policy_version");
var findingId = runtimeEntityType.AddProperty("FindingId", typeof(string),
propertyInfo: typeof(VerdictAttestationEntity).GetProperty("FindingId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(VerdictAttestationEntity).GetField("<FindingId>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
findingId.AddAnnotation("Relational:ColumnName", "finding_id");
var verdictStatus = runtimeEntityType.AddProperty("VerdictStatus", typeof(string),
propertyInfo: typeof(VerdictAttestationEntity).GetProperty("VerdictStatus", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(VerdictAttestationEntity).GetField("<VerdictStatus>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
verdictStatus.AddAnnotation("Relational:ColumnName", "verdict_status");
var verdictSeverity = runtimeEntityType.AddProperty("VerdictSeverity", typeof(string),
propertyInfo: typeof(VerdictAttestationEntity).GetProperty("VerdictSeverity", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(VerdictAttestationEntity).GetField("<VerdictSeverity>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
verdictSeverity.AddAnnotation("Relational:ColumnName", "verdict_severity");
var verdictScore = runtimeEntityType.AddProperty("VerdictScore", typeof(decimal),
propertyInfo: typeof(VerdictAttestationEntity).GetProperty("VerdictScore", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(VerdictAttestationEntity).GetField("<VerdictScore>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
sentinel: 0m);
verdictScore.AddAnnotation("Relational:ColumnName", "verdict_score");
verdictScore.AddAnnotation("Relational:ColumnType", "numeric(5,2)");
var evaluatedAt = runtimeEntityType.AddProperty("EvaluatedAt", typeof(DateTime),
propertyInfo: typeof(VerdictAttestationEntity).GetProperty("EvaluatedAt", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(VerdictAttestationEntity).GetField("<EvaluatedAt>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
sentinel: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
evaluatedAt.AddAnnotation("Relational:ColumnName", "evaluated_at");
var envelope = runtimeEntityType.AddProperty("Envelope", typeof(string),
propertyInfo: typeof(VerdictAttestationEntity).GetProperty("Envelope", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(VerdictAttestationEntity).GetField("<Envelope>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
envelope.AddAnnotation("Relational:ColumnName", "envelope");
envelope.AddAnnotation("Relational:ColumnType", "jsonb");
var predicateDigest = runtimeEntityType.AddProperty("PredicateDigest", typeof(string),
propertyInfo: typeof(VerdictAttestationEntity).GetProperty("PredicateDigest", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(VerdictAttestationEntity).GetField("<PredicateDigest>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
predicateDigest.AddAnnotation("Relational:ColumnName", "predicate_digest");
var determinismHash = runtimeEntityType.AddProperty("DeterminismHash", typeof(string),
propertyInfo: typeof(VerdictAttestationEntity).GetProperty("DeterminismHash", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(VerdictAttestationEntity).GetField("<DeterminismHash>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
nullable: true);
determinismHash.AddAnnotation("Relational:ColumnName", "determinism_hash");
var rekorLogIndex = runtimeEntityType.AddProperty("RekorLogIndex", typeof(long?),
propertyInfo: typeof(VerdictAttestationEntity).GetProperty("RekorLogIndex", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(VerdictAttestationEntity).GetField("<RekorLogIndex>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
nullable: true);
rekorLogIndex.AddAnnotation("Relational:ColumnName", "rekor_log_index");
var createdAt = runtimeEntityType.AddProperty("CreatedAt", typeof(DateTime),
propertyInfo: typeof(VerdictAttestationEntity).GetProperty("CreatedAt", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(VerdictAttestationEntity).GetField("<CreatedAt>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
valueGenerated: ValueGenerated.OnAdd, sentinel: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
createdAt.AddAnnotation("Relational:ColumnName", "created_at");
createdAt.AddAnnotation("Relational:DefaultValueSql", "NOW()");
var updatedAt = runtimeEntityType.AddProperty("UpdatedAt", typeof(DateTime),
propertyInfo: typeof(VerdictAttestationEntity).GetProperty("UpdatedAt", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(VerdictAttestationEntity).GetField("<UpdatedAt>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
valueGenerated: ValueGenerated.OnAdd, sentinel: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
updatedAt.AddAnnotation("Relational:ColumnName", "updated_at");
updatedAt.AddAnnotation("Relational:DefaultValueSql", "NOW()");
var key = runtimeEntityType.AddKey(new[] { verdictId });
runtimeEntityType.SetPrimaryKey(key);
key.AddAnnotation("Relational:Name", "verdict_attestations_pkey");
runtimeEntityType.AddIndex(new[] { runId }, name: "idx_verdict_attestations_run");
runtimeEntityType.AddIndex(new[] { findingId }, name: "idx_verdict_attestations_finding");
runtimeEntityType.AddIndex(new[] { tenantId, evaluatedAt }, name: "idx_verdict_attestations_tenant_evaluated");
runtimeEntityType.AddIndex(new[] { tenantId, verdictStatus }, name: "idx_verdict_attestations_tenant_status");
runtimeEntityType.AddIndex(new[] { tenantId, verdictSeverity }, name: "idx_verdict_attestations_tenant_severity");
runtimeEntityType.AddIndex(new[] { policyId, policyVersion }, name: "idx_verdict_attestations_policy");
return runtimeEntityType;
}
public static void CreateAnnotations(RuntimeEntityType runtimeEntityType)
{
runtimeEntityType.AddAnnotation("Relational:Schema", "evidence_locker");
runtimeEntityType.AddAnnotation("Relational:TableName", "verdict_attestations");
Customize(runtimeEntityType);
}
static partial void Customize(RuntimeEntityType runtimeEntityType);
}
}

View File

@@ -0,0 +1,37 @@
using Microsoft.EntityFrameworkCore;
using StellaOps.EvidenceLocker.Infrastructure.EfCore.Models;
namespace StellaOps.EvidenceLocker.Infrastructure.EfCore.Context;
public partial class EvidenceLockerDbContext
{
partial void OnModelCreatingPartial(ModelBuilder modelBuilder)
{
modelBuilder.Entity<EvidenceBundleSignatureEntity>(entity =>
{
entity.HasOne(e => e.Bundle)
.WithOne(b => b.Signature)
.HasForeignKey<EvidenceBundleSignatureEntity>(e => e.BundleId)
.HasConstraintName("fk_evidence_bundle_signatures_bundle")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity<EvidenceArtifactEntity>(entity =>
{
entity.HasOne(e => e.Bundle)
.WithMany(b => b.Artifacts)
.HasForeignKey(e => e.BundleId)
.HasConstraintName("fk_artifacts_bundle")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity<EvidenceHoldEntity>(entity =>
{
entity.HasOne(e => e.Bundle)
.WithMany(b => b.Holds)
.HasForeignKey(e => e.BundleId)
.HasConstraintName("fk_evidence_holds_bundle")
.OnDelete(DeleteBehavior.SetNull);
});
}
}

View File

@@ -0,0 +1,217 @@
using Microsoft.EntityFrameworkCore;
using StellaOps.EvidenceLocker.Infrastructure.EfCore.Models;
namespace StellaOps.EvidenceLocker.Infrastructure.EfCore.Context;
public partial class EvidenceLockerDbContext : DbContext
{
private readonly string _schemaName;
public EvidenceLockerDbContext(DbContextOptions<EvidenceLockerDbContext> options, string? schemaName = null)
: base(options)
{
_schemaName = string.IsNullOrWhiteSpace(schemaName)
? "evidence_locker"
: schemaName.Trim();
}
public virtual DbSet<EvidenceBundleEntity> EvidenceBundles { get; set; }
public virtual DbSet<EvidenceBundleSignatureEntity> EvidenceBundleSignatures { get; set; }
public virtual DbSet<EvidenceArtifactEntity> EvidenceArtifacts { get; set; }
public virtual DbSet<EvidenceHoldEntity> EvidenceHolds { get; set; }
public virtual DbSet<EvidenceGateArtifactEntity> EvidenceGateArtifacts { get; set; }
public virtual DbSet<VerdictAttestationEntity> VerdictAttestations { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var schemaName = _schemaName;
modelBuilder.Entity<EvidenceBundleEntity>(entity =>
{
entity.HasKey(e => e.BundleId).HasName("evidence_bundles_pkey");
entity.ToTable("evidence_bundles", schemaName);
entity.HasIndex(e => new { e.TenantId, e.StorageKey }, "uq_evidence_bundles_storage_key")
.IsUnique();
entity.HasIndex(e => new { e.TenantId, e.PortableStorageKey }, "uq_evidence_bundles_portable_storage_key")
.IsUnique()
.HasFilter("portable_storage_key IS NOT NULL");
entity.Property(e => e.BundleId).HasColumnName("bundle_id");
entity.Property(e => e.TenantId).HasColumnName("tenant_id");
entity.Property(e => e.Kind).HasColumnName("kind");
entity.Property(e => e.Status).HasColumnName("status");
entity.Property(e => e.RootHash).HasColumnName("root_hash");
entity.Property(e => e.StorageKey).HasColumnName("storage_key");
entity.Property(e => e.Description).HasColumnName("description");
entity.Property(e => e.SealedAt).HasColumnName("sealed_at");
entity.Property(e => e.ExpiresAt).HasColumnName("expires_at");
entity.Property(e => e.CreatedAt)
.HasDefaultValueSql("(NOW() AT TIME ZONE 'UTC')")
.HasColumnName("created_at");
entity.Property(e => e.UpdatedAt)
.HasDefaultValueSql("(NOW() AT TIME ZONE 'UTC')")
.HasColumnName("updated_at");
entity.Property(e => e.PortableStorageKey).HasColumnName("portable_storage_key");
entity.Property(e => e.PortableGeneratedAt).HasColumnName("portable_generated_at");
});
modelBuilder.Entity<EvidenceBundleSignatureEntity>(entity =>
{
entity.HasKey(e => new { e.BundleId, e.TenantId }).HasName("evidence_bundle_signatures_pkey");
entity.ToTable("evidence_bundle_signatures", schemaName);
entity.HasIndex(e => new { e.TenantId, e.SignedAt }, "ix_evidence_bundle_signatures_signed_at")
.IsDescending(false, true);
entity.Property(e => e.BundleId).HasColumnName("bundle_id");
entity.Property(e => e.TenantId).HasColumnName("tenant_id");
entity.Property(e => e.PayloadType).HasColumnName("payload_type");
entity.Property(e => e.Payload).HasColumnName("payload");
entity.Property(e => e.Signature).HasColumnName("signature");
entity.Property(e => e.KeyId).HasColumnName("key_id");
entity.Property(e => e.Algorithm).HasColumnName("algorithm");
entity.Property(e => e.Provider).HasColumnName("provider");
entity.Property(e => e.SignedAt).HasColumnName("signed_at");
entity.Property(e => e.TimestampedAt).HasColumnName("timestamped_at");
entity.Property(e => e.TimestampAuthority).HasColumnName("timestamp_authority");
entity.Property(e => e.TimestampToken).HasColumnName("timestamp_token");
});
modelBuilder.Entity<EvidenceArtifactEntity>(entity =>
{
entity.HasKey(e => e.ArtifactId).HasName("evidence_artifacts_pkey");
entity.ToTable("evidence_artifacts", schemaName);
entity.HasIndex(e => e.BundleId, "ix_evidence_artifacts_bundle_id");
entity.HasIndex(e => new { e.TenantId, e.StorageKey }, "uq_evidence_artifacts_storage_key")
.IsUnique();
entity.Property(e => e.ArtifactId).HasColumnName("artifact_id");
entity.Property(e => e.BundleId).HasColumnName("bundle_id");
entity.Property(e => e.TenantId).HasColumnName("tenant_id");
entity.Property(e => e.Name).HasColumnName("name");
entity.Property(e => e.ContentType).HasColumnName("content_type");
entity.Property(e => e.SizeBytes).HasColumnName("size_bytes");
entity.Property(e => e.StorageKey).HasColumnName("storage_key");
entity.Property(e => e.Sha256).HasColumnName("sha256");
entity.Property(e => e.CreatedAt)
.HasDefaultValueSql("(NOW() AT TIME ZONE 'UTC')")
.HasColumnName("created_at");
});
modelBuilder.Entity<EvidenceHoldEntity>(entity =>
{
entity.HasKey(e => e.HoldId).HasName("evidence_holds_pkey");
entity.ToTable("evidence_holds", schemaName);
entity.HasIndex(e => new { e.TenantId, e.CaseId }, "uq_evidence_holds_case")
.IsUnique();
entity.Property(e => e.HoldId).HasColumnName("hold_id");
entity.Property(e => e.TenantId).HasColumnName("tenant_id");
entity.Property(e => e.BundleId).HasColumnName("bundle_id");
entity.Property(e => e.CaseId).HasColumnName("case_id");
entity.Property(e => e.Reason).HasColumnName("reason");
entity.Property(e => e.Notes).HasColumnName("notes");
entity.Property(e => e.CreatedAt)
.HasDefaultValueSql("(NOW() AT TIME ZONE 'UTC')")
.HasColumnName("created_at");
entity.Property(e => e.ExpiresAt).HasColumnName("expires_at");
entity.Property(e => e.ReleasedAt).HasColumnName("released_at");
});
modelBuilder.Entity<EvidenceGateArtifactEntity>(entity =>
{
entity.HasKey(e => new { e.TenantId, e.ArtifactId }).HasName("pk_evidence_gate_artifacts");
entity.ToTable("evidence_gate_artifacts", schemaName);
entity.HasIndex(e => e.EvidenceId, "uq_evidence_gate_artifacts_evidence_id")
.IsUnique();
entity.HasIndex(e => new { e.TenantId, e.EvidenceScore }, "ix_evidence_gate_artifacts_tenant_score");
entity.Property(e => e.EvidenceId).HasColumnName("evidence_id");
entity.Property(e => e.TenantId).HasColumnName("tenant_id");
entity.Property(e => e.ArtifactId).HasColumnName("artifact_id");
entity.Property(e => e.CanonicalBomSha256).HasColumnName("canonical_bom_sha256");
entity.Property(e => e.PayloadDigest).HasColumnName("payload_digest");
entity.Property(e => e.DsseEnvelopeRef).HasColumnName("dsse_envelope_ref");
entity.Property(e => e.RekorIndex).HasColumnName("rekor_index");
entity.Property(e => e.RekorTileId).HasColumnName("rekor_tile_id");
entity.Property(e => e.RekorInclusionProofRef).HasColumnName("rekor_inclusion_proof_ref");
entity.Property(e => e.AttestationRefs)
.HasDefaultValueSql("'[]'::jsonb")
.HasColumnType("jsonb")
.HasColumnName("attestation_refs");
entity.Property(e => e.RawBomRef).HasColumnName("raw_bom_ref");
entity.Property(e => e.VexRefs)
.HasDefaultValueSql("'[]'::jsonb")
.HasColumnType("jsonb")
.HasColumnName("vex_refs");
entity.Property(e => e.EvidenceScore).HasColumnName("evidence_score");
entity.Property(e => e.CreatedAt)
.HasDefaultValueSql("(NOW() AT TIME ZONE 'UTC')")
.HasColumnName("created_at");
entity.Property(e => e.UpdatedAt)
.HasDefaultValueSql("(NOW() AT TIME ZONE 'UTC')")
.HasColumnName("updated_at");
});
modelBuilder.Entity<VerdictAttestationEntity>(entity =>
{
entity.HasKey(e => e.VerdictId).HasName("verdict_attestations_pkey");
entity.ToTable("verdict_attestations", schemaName);
entity.HasIndex(e => e.RunId, "idx_verdict_attestations_run");
entity.HasIndex(e => e.FindingId, "idx_verdict_attestations_finding");
entity.HasIndex(e => new { e.TenantId, e.EvaluatedAt }, "idx_verdict_attestations_tenant_evaluated")
.IsDescending(false, true);
entity.HasIndex(e => new { e.TenantId, e.VerdictStatus }, "idx_verdict_attestations_tenant_status");
entity.HasIndex(e => new { e.TenantId, e.VerdictSeverity }, "idx_verdict_attestations_tenant_severity");
entity.HasIndex(e => new { e.PolicyId, e.PolicyVersion }, "idx_verdict_attestations_policy");
entity.Property(e => e.VerdictId).HasColumnName("verdict_id");
entity.Property(e => e.TenantId).HasColumnName("tenant_id");
entity.Property(e => e.RunId).HasColumnName("run_id");
entity.Property(e => e.PolicyId).HasColumnName("policy_id");
entity.Property(e => e.PolicyVersion).HasColumnName("policy_version");
entity.Property(e => e.FindingId).HasColumnName("finding_id");
entity.Property(e => e.VerdictStatus).HasColumnName("verdict_status");
entity.Property(e => e.VerdictSeverity).HasColumnName("verdict_severity");
entity.Property(e => e.VerdictScore)
.HasColumnType("numeric(5,2)")
.HasColumnName("verdict_score");
entity.Property(e => e.EvaluatedAt).HasColumnName("evaluated_at");
entity.Property(e => e.Envelope)
.HasColumnType("jsonb")
.HasColumnName("envelope");
entity.Property(e => e.PredicateDigest).HasColumnName("predicate_digest");
entity.Property(e => e.DeterminismHash).HasColumnName("determinism_hash");
entity.Property(e => e.RekorLogIndex).HasColumnName("rekor_log_index");
entity.Property(e => e.CreatedAt)
.HasDefaultValueSql("NOW()")
.HasColumnName("created_at");
entity.Property(e => e.UpdatedAt)
.HasDefaultValueSql("NOW()")
.HasColumnName("updated_at");
});
OnModelCreatingPartial(modelBuilder);
}
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}

View File

@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
namespace StellaOps.EvidenceLocker.Infrastructure.EfCore.Context;
public sealed class EvidenceLockerDesignTimeDbContextFactory : IDesignTimeDbContextFactory<EvidenceLockerDbContext>
{
private const string DefaultConnectionString =
"Host=localhost;Port=55433;Database=postgres;Username=postgres;Password=postgres;Search Path=evidence_locker,public";
private const string ConnectionStringEnvironmentVariable =
"STELLAOPS_EVIDENCELOCKER_EF_CONNECTION";
public EvidenceLockerDbContext CreateDbContext(string[] args)
{
var connectionString = ResolveConnectionString();
var options = new DbContextOptionsBuilder<EvidenceLockerDbContext>()
.UseNpgsql(connectionString)
.Options;
return new EvidenceLockerDbContext(options);
}
private static string ResolveConnectionString()
{
var fromEnvironment = Environment.GetEnvironmentVariable(ConnectionStringEnvironmentVariable);
return string.IsNullOrWhiteSpace(fromEnvironment) ? DefaultConnectionString : fromEnvironment;
}
}

View File

@@ -0,0 +1,6 @@
namespace StellaOps.EvidenceLocker.Infrastructure.EfCore.Models;
public partial class EvidenceArtifactEntity
{
public virtual EvidenceBundleEntity Bundle { get; set; } = null!;
}

View File

@@ -0,0 +1,22 @@
namespace StellaOps.EvidenceLocker.Infrastructure.EfCore.Models;
public partial class EvidenceArtifactEntity
{
public Guid ArtifactId { get; set; }
public Guid BundleId { get; set; }
public Guid TenantId { get; set; }
public string Name { get; set; } = null!;
public string ContentType { get; set; } = null!;
public long SizeBytes { get; set; }
public string StorageKey { get; set; } = null!;
public string Sha256 { get; set; } = null!;
public DateTime CreatedAt { get; set; }
}

View File

@@ -0,0 +1,10 @@
namespace StellaOps.EvidenceLocker.Infrastructure.EfCore.Models;
public partial class EvidenceBundleEntity
{
public virtual EvidenceBundleSignatureEntity? Signature { get; set; }
public virtual ICollection<EvidenceArtifactEntity> Artifacts { get; set; } = new List<EvidenceArtifactEntity>();
public virtual ICollection<EvidenceHoldEntity> Holds { get; set; } = new List<EvidenceHoldEntity>();
}

View File

@@ -0,0 +1,30 @@
namespace StellaOps.EvidenceLocker.Infrastructure.EfCore.Models;
public partial class EvidenceBundleEntity
{
public Guid BundleId { get; set; }
public Guid TenantId { get; set; }
public short Kind { get; set; }
public short Status { get; set; }
public string RootHash { get; set; } = null!;
public string StorageKey { get; set; } = null!;
public string? Description { get; set; }
public DateTime? SealedAt { get; set; }
public DateTime? ExpiresAt { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public string? PortableStorageKey { get; set; }
public DateTime? PortableGeneratedAt { get; set; }
}

View File

@@ -0,0 +1,6 @@
namespace StellaOps.EvidenceLocker.Infrastructure.EfCore.Models;
public partial class EvidenceBundleSignatureEntity
{
public virtual EvidenceBundleEntity Bundle { get; set; } = null!;
}

View File

@@ -0,0 +1,28 @@
namespace StellaOps.EvidenceLocker.Infrastructure.EfCore.Models;
public partial class EvidenceBundleSignatureEntity
{
public Guid BundleId { get; set; }
public Guid TenantId { get; set; }
public string PayloadType { get; set; } = null!;
public string Payload { get; set; } = null!;
public string Signature { get; set; } = null!;
public string? KeyId { get; set; }
public string Algorithm { get; set; } = null!;
public string Provider { get; set; } = null!;
public DateTime SignedAt { get; set; }
public DateTime? TimestampedAt { get; set; }
public string? TimestampAuthority { get; set; }
public byte[]? TimestampToken { get; set; }
}

View File

@@ -0,0 +1,34 @@
namespace StellaOps.EvidenceLocker.Infrastructure.EfCore.Models;
public partial class EvidenceGateArtifactEntity
{
public string EvidenceId { get; set; } = null!;
public Guid TenantId { get; set; }
public string ArtifactId { get; set; } = null!;
public string CanonicalBomSha256 { get; set; } = null!;
public string PayloadDigest { get; set; } = null!;
public string DsseEnvelopeRef { get; set; } = null!;
public long RekorIndex { get; set; }
public string RekorTileId { get; set; } = null!;
public string RekorInclusionProofRef { get; set; } = null!;
public string AttestationRefs { get; set; } = null!;
public string? RawBomRef { get; set; }
public string VexRefs { get; set; } = null!;
public string EvidenceScore { get; set; } = null!;
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}

View File

@@ -0,0 +1,6 @@
namespace StellaOps.EvidenceLocker.Infrastructure.EfCore.Models;
public partial class EvidenceHoldEntity
{
public virtual EvidenceBundleEntity? Bundle { get; set; }
}

View File

@@ -0,0 +1,22 @@
namespace StellaOps.EvidenceLocker.Infrastructure.EfCore.Models;
public partial class EvidenceHoldEntity
{
public Guid HoldId { get; set; }
public Guid TenantId { get; set; }
public Guid? BundleId { get; set; }
public string CaseId { get; set; } = null!;
public string Reason { get; set; } = null!;
public string? Notes { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? ExpiresAt { get; set; }
public DateTime? ReleasedAt { get; set; }
}

View File

@@ -0,0 +1,36 @@
namespace StellaOps.EvidenceLocker.Infrastructure.EfCore.Models;
public partial class VerdictAttestationEntity
{
public string VerdictId { get; set; } = null!;
public string TenantId { get; set; } = null!;
public string RunId { get; set; } = null!;
public string PolicyId { get; set; } = null!;
public int PolicyVersion { get; set; }
public string FindingId { get; set; } = null!;
public string VerdictStatus { get; set; } = null!;
public string VerdictSeverity { get; set; } = null!;
public decimal VerdictScore { get; set; }
public DateTime EvaluatedAt { get; set; }
public string Envelope { get; set; } = null!;
public string PredicateDigest { get; set; } = null!;
public string? DeterminismHash { get; set; }
public long? RekorLogIndex { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}

View File

@@ -1,149 +1,35 @@
using Microsoft.EntityFrameworkCore;
using Npgsql;
using NpgsqlTypes;
using StellaOps.EvidenceLocker.Core.Domain;
using StellaOps.EvidenceLocker.Core.Repositories;
using StellaOps.EvidenceLocker.Infrastructure.Db;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using StellaOps.EvidenceLocker.Infrastructure.EfCore.Models;
namespace StellaOps.EvidenceLocker.Infrastructure.Repositories;
internal sealed class EvidenceBundleRepository(EvidenceLockerDataSource dataSource) : IEvidenceBundleRepository
{
private const string InsertBundleSql = """
INSERT INTO evidence_locker.evidence_bundles
(bundle_id, tenant_id, kind, status, root_hash, storage_key, description, created_at, updated_at)
VALUES
(@bundle_id, @tenant_id, @kind, @status, @root_hash, @storage_key, @description, @created_at, @updated_at);
""";
private const string UpdateBundleSql = """
UPDATE evidence_locker.evidence_bundles
SET status = @status,
root_hash = @root_hash,
updated_at = @updated_at
WHERE bundle_id = @bundle_id
AND tenant_id = @tenant_id;
""";
private const string MarkBundleSealedSql = """
UPDATE evidence_locker.evidence_bundles
SET status = @status,
sealed_at = @sealed_at,
updated_at = @sealed_at
WHERE bundle_id = @bundle_id
AND tenant_id = @tenant_id;
""";
private const string UpsertSignatureSql = """
INSERT INTO evidence_locker.evidence_bundle_signatures
(bundle_id, tenant_id, payload_type, payload, signature, key_id, algorithm, provider, signed_at, timestamped_at, timestamp_authority, timestamp_token)
VALUES
(@bundle_id, @tenant_id, @payload_type, @payload, @signature, @key_id, @algorithm, @provider, @signed_at, @timestamped_at, @timestamp_authority, @timestamp_token)
ON CONFLICT (bundle_id, tenant_id)
DO UPDATE SET
payload_type = EXCLUDED.payload_type,
payload = EXCLUDED.payload,
signature = EXCLUDED.signature,
key_id = EXCLUDED.key_id,
algorithm = EXCLUDED.algorithm,
provider = EXCLUDED.provider,
signed_at = EXCLUDED.signed_at,
timestamped_at = EXCLUDED.timestamped_at,
timestamp_authority = EXCLUDED.timestamp_authority,
timestamp_token = EXCLUDED.timestamp_token;
""";
private const string SelectBundleSql = """
SELECT b.bundle_id, b.tenant_id, b.kind, b.status, b.root_hash, b.storage_key, b.description, b.sealed_at, b.created_at, b.updated_at, b.expires_at,
b.portable_storage_key, b.portable_generated_at,
s.payload_type, s.payload, s.signature, s.key_id, s.algorithm, s.provider, s.signed_at, s.timestamped_at, s.timestamp_authority, s.timestamp_token
FROM evidence_locker.evidence_bundles b
LEFT JOIN evidence_locker.evidence_bundle_signatures s
ON s.bundle_id = b.bundle_id AND s.tenant_id = b.tenant_id
WHERE b.bundle_id = @bundle_id AND b.tenant_id = @tenant_id;
""";
private const string ExistsSql = """
SELECT 1
FROM evidence_locker.evidence_bundles
WHERE bundle_id = @bundle_id AND tenant_id = @tenant_id;
""";
private const string SelectBundlesForReindexSql = """
SELECT b.bundle_id, b.tenant_id, b.kind, b.status, b.root_hash, b.storage_key, b.description, b.sealed_at, b.created_at, b.updated_at, b.expires_at,
b.portable_storage_key, b.portable_generated_at,
s.payload_type, s.payload, s.signature, s.key_id, s.algorithm, s.provider, s.signed_at, s.timestamped_at, s.timestamp_authority, s.timestamp_token
FROM evidence_locker.evidence_bundles b
LEFT JOIN evidence_locker.evidence_bundle_signatures s
ON s.bundle_id = b.bundle_id AND s.tenant_id = b.tenant_id
WHERE b.tenant_id = @tenant_id
AND b.status = @status
AND (@since IS NULL OR b.updated_at >= @since)
AND (
@cursor_updated_at IS NULL OR
(b.updated_at, b.bundle_id) > (@cursor_updated_at, @cursor_bundle_id)
)
ORDER BY b.updated_at, b.bundle_id
LIMIT @limit;
""";
private const string InsertHoldSql = """
INSERT INTO evidence_locker.evidence_holds
(hold_id, tenant_id, bundle_id, case_id, reason, notes, created_at, expires_at)
VALUES
(@hold_id, @tenant_id, @bundle_id, @case_id, @reason, @notes, @created_at, @expires_at)
RETURNING hold_id, tenant_id, bundle_id, case_id, reason, notes, created_at, expires_at, released_at;
""";
private const string ExtendRetentionSql = """
UPDATE evidence_locker.evidence_bundles
SET expires_at = CASE
WHEN @hold_expires_at IS NULL THEN NULL
WHEN expires_at IS NULL THEN @hold_expires_at
WHEN expires_at < @hold_expires_at THEN @hold_expires_at
ELSE expires_at
END,
updated_at = GREATEST(updated_at, @processed_at)
WHERE bundle_id = @bundle_id
AND tenant_id = @tenant_id;
""";
private const string UpdateStorageKeySql = """
UPDATE evidence_locker.evidence_bundles
SET storage_key = @storage_key,
updated_at = NOW() AT TIME ZONE 'UTC'
WHERE bundle_id = @bundle_id
AND tenant_id = @tenant_id;
""";
private const string UpdatePortableStorageKeySql = """
UPDATE evidence_locker.evidence_bundles
SET portable_storage_key = @storage_key,
portable_generated_at = @generated_at,
updated_at = GREATEST(updated_at, @generated_at)
WHERE bundle_id = @bundle_id
AND tenant_id = @tenant_id;
""";
private const int CommandTimeoutSeconds = 30;
public async Task CreateBundleAsync(EvidenceBundle bundle, CancellationToken cancellationToken)
{
await using var connection = await dataSource.OpenConnectionAsync(bundle.TenantId, cancellationToken);
await using var command = new NpgsqlCommand(InsertBundleSql, connection);
command.Parameters.AddWithValue("bundle_id", bundle.Id.Value);
command.Parameters.AddWithValue("tenant_id", bundle.TenantId.Value);
command.Parameters.AddWithValue("kind", (int)bundle.Kind);
command.Parameters.AddWithValue("status", (int)bundle.Status);
command.Parameters.AddWithValue("root_hash", bundle.RootHash);
command.Parameters.AddWithValue("storage_key", bundle.StorageKey);
command.Parameters.AddWithValue("description", (object?)bundle.Description ?? DBNull.Value);
command.Parameters.AddWithValue("created_at", bundle.CreatedAt.UtcDateTime);
command.Parameters.AddWithValue("updated_at", bundle.UpdatedAt.UtcDateTime);
await command.ExecuteNonQueryAsync(cancellationToken);
await using var dbContext = EvidenceLockerDbContextFactory.Create(connection, CommandTimeoutSeconds, EvidenceLockerDbContextFactory.DefaultSchemaName);
dbContext.EvidenceBundles.Add(new EvidenceBundleEntity
{
BundleId = bundle.Id.Value,
TenantId = bundle.TenantId.Value,
Kind = (short)bundle.Kind,
Status = (short)bundle.Status,
RootHash = bundle.RootHash,
StorageKey = bundle.StorageKey,
Description = bundle.Description,
CreatedAt = bundle.CreatedAt.UtcDateTime,
UpdatedAt = bundle.UpdatedAt.UtcDateTime
});
await dbContext.SaveChangesAsync(cancellationToken);
}
public async Task SetBundleAssemblyAsync(
@@ -155,14 +41,16 @@ internal sealed class EvidenceBundleRepository(EvidenceLockerDataSource dataSour
CancellationToken cancellationToken)
{
await using var connection = await dataSource.OpenConnectionAsync(tenantId, cancellationToken);
await using var command = new NpgsqlCommand(UpdateBundleSql, connection);
command.Parameters.AddWithValue("status", (int)status);
command.Parameters.AddWithValue("root_hash", rootHash);
command.Parameters.AddWithValue("updated_at", updatedAt.UtcDateTime);
command.Parameters.AddWithValue("bundle_id", bundleId.Value);
command.Parameters.AddWithValue("tenant_id", tenantId.Value);
await using var dbContext = EvidenceLockerDbContextFactory.Create(connection, CommandTimeoutSeconds, EvidenceLockerDbContextFactory.DefaultSchemaName);
var affected = await dbContext.EvidenceBundles
.Where(b => b.BundleId == bundleId.Value && b.TenantId == tenantId.Value)
.ExecuteUpdateAsync(setters => setters
.SetProperty(b => b.Status, (short)status)
.SetProperty(b => b.RootHash, rootHash)
.SetProperty(b => b.UpdatedAt, updatedAt.UtcDateTime),
cancellationToken);
var affected = await command.ExecuteNonQueryAsync(cancellationToken);
if (affected == 0)
{
throw new InvalidOperationException("Evidence bundle record not found for update.");
@@ -177,13 +65,16 @@ internal sealed class EvidenceBundleRepository(EvidenceLockerDataSource dataSour
CancellationToken cancellationToken)
{
await using var connection = await dataSource.OpenConnectionAsync(tenantId, cancellationToken);
await using var command = new NpgsqlCommand(MarkBundleSealedSql, connection);
command.Parameters.AddWithValue("status", (int)status);
command.Parameters.AddWithValue("sealed_at", sealedAt.UtcDateTime);
command.Parameters.AddWithValue("bundle_id", bundleId.Value);
command.Parameters.AddWithValue("tenant_id", tenantId.Value);
await using var dbContext = EvidenceLockerDbContextFactory.Create(connection, CommandTimeoutSeconds, EvidenceLockerDbContextFactory.DefaultSchemaName);
var affected = await dbContext.EvidenceBundles
.Where(b => b.BundleId == bundleId.Value && b.TenantId == tenantId.Value)
.ExecuteUpdateAsync(setters => setters
.SetProperty(b => b.Status, (short)status)
.SetProperty(b => b.SealedAt, sealedAt.UtcDateTime)
.SetProperty(b => b.UpdatedAt, sealedAt.UtcDateTime),
cancellationToken);
var affected = await command.ExecuteNonQueryAsync(cancellationToken);
if (affected == 0)
{
throw new InvalidOperationException("Evidence bundle record not found for sealing.");
@@ -192,39 +83,65 @@ internal sealed class EvidenceBundleRepository(EvidenceLockerDataSource dataSour
public async Task UpsertSignatureAsync(EvidenceBundleSignature signature, CancellationToken cancellationToken)
{
// Use raw SQL for UPSERT with ON CONFLICT as the multi-column conflict clause
// is more natural in SQL than EF's catch-and-update pattern for composite keys.
await using var connection = await dataSource.OpenConnectionAsync(signature.TenantId, cancellationToken);
await using var command = new NpgsqlCommand(UpsertSignatureSql, connection);
command.Parameters.AddWithValue("bundle_id", signature.BundleId.Value);
command.Parameters.AddWithValue("tenant_id", signature.TenantId.Value);
command.Parameters.AddWithValue("payload_type", signature.PayloadType);
command.Parameters.AddWithValue("payload", signature.Payload);
command.Parameters.AddWithValue("signature", signature.Signature);
command.Parameters.AddWithValue("key_id", (object?)signature.KeyId ?? DBNull.Value);
command.Parameters.AddWithValue("algorithm", signature.Algorithm);
command.Parameters.AddWithValue("provider", signature.Provider);
command.Parameters.AddWithValue("signed_at", signature.SignedAt.UtcDateTime);
command.Parameters.AddWithValue("timestamped_at", signature.TimestampedAt?.UtcDateTime ?? (object)DBNull.Value);
command.Parameters.AddWithValue("timestamp_authority", (object?)signature.TimestampAuthority ?? DBNull.Value);
var timestampTokenParameter = command.Parameters.Add("timestamp_token", NpgsqlDbType.Bytea);
timestampTokenParameter.Value = signature.TimestampToken ?? (object)DBNull.Value;
await using var dbContext = EvidenceLockerDbContextFactory.Create(connection, CommandTimeoutSeconds, EvidenceLockerDbContextFactory.DefaultSchemaName);
await command.ExecuteNonQueryAsync(cancellationToken);
await dbContext.Database.ExecuteSqlRawAsync("""
INSERT INTO evidence_locker.evidence_bundle_signatures
(bundle_id, tenant_id, payload_type, payload, signature, key_id, algorithm, provider, signed_at, timestamped_at, timestamp_authority, timestamp_token)
VALUES
({0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {10}, {11})
ON CONFLICT (bundle_id, tenant_id)
DO UPDATE SET
payload_type = EXCLUDED.payload_type,
payload = EXCLUDED.payload,
signature = EXCLUDED.signature,
key_id = EXCLUDED.key_id,
algorithm = EXCLUDED.algorithm,
provider = EXCLUDED.provider,
signed_at = EXCLUDED.signed_at,
timestamped_at = EXCLUDED.timestamped_at,
timestamp_authority = EXCLUDED.timestamp_authority,
timestamp_token = EXCLUDED.timestamp_token
""",
signature.BundleId.Value,
signature.TenantId.Value,
signature.PayloadType,
signature.Payload,
signature.Signature,
(object?)signature.KeyId ?? DBNull.Value,
signature.Algorithm,
signature.Provider,
signature.SignedAt.UtcDateTime,
(object?)signature.TimestampedAt?.UtcDateTime ?? DBNull.Value,
(object?)signature.TimestampAuthority ?? DBNull.Value,
(object?)signature.TimestampToken ?? DBNull.Value,
cancellationToken);
}
public async Task<EvidenceBundleDetails?> GetBundleAsync(EvidenceBundleId bundleId, TenantId tenantId, CancellationToken cancellationToken)
{
await using var connection = await dataSource.OpenConnectionAsync(tenantId, cancellationToken);
await using var command = new NpgsqlCommand(SelectBundleSql, connection);
command.Parameters.AddWithValue("bundle_id", bundleId.Value);
command.Parameters.AddWithValue("tenant_id", tenantId.Value);
await using var dbContext = EvidenceLockerDbContextFactory.Create(connection, CommandTimeoutSeconds, EvidenceLockerDbContextFactory.DefaultSchemaName);
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
if (!await reader.ReadAsync(cancellationToken))
var bundleEntity = await dbContext.EvidenceBundles
.AsNoTracking()
.Where(b => b.BundleId == bundleId.Value && b.TenantId == tenantId.Value)
.FirstOrDefaultAsync(cancellationToken);
if (bundleEntity is null)
{
return null;
}
return MapBundleDetails(reader);
var signatureEntity = await dbContext.EvidenceBundleSignatures
.AsNoTracking()
.Where(s => s.BundleId == bundleId.Value && s.TenantId == tenantId.Value)
.FirstOrDefaultAsync(cancellationToken);
return MapBundleDetails(bundleEntity, signatureEntity);
}
public async Task<IReadOnlyList<EvidenceBundleDetails>> GetBundlesForReindexAsync(
@@ -236,157 +153,91 @@ internal sealed class EvidenceBundleRepository(EvidenceLockerDataSource dataSour
CancellationToken cancellationToken)
{
await using var connection = await dataSource.OpenConnectionAsync(tenantId, cancellationToken);
await using var command = new NpgsqlCommand(SelectBundlesForReindexSql, connection);
command.Parameters.AddWithValue("tenant_id", tenantId.Value);
command.Parameters.AddWithValue("status", (int)EvidenceBundleStatus.Sealed);
command.Parameters.AddWithValue("since", (object?)since?.UtcDateTime ?? DBNull.Value);
command.Parameters.AddWithValue("cursor_updated_at", (object?)cursorUpdatedAt?.UtcDateTime ?? DBNull.Value);
command.Parameters.AddWithValue("cursor_bundle_id", (object?)cursorBundleId?.Value ?? DBNull.Value);
command.Parameters.AddWithValue("limit", limit);
await using var dbContext = EvidenceLockerDbContextFactory.Create(connection, CommandTimeoutSeconds, EvidenceLockerDbContextFactory.DefaultSchemaName);
var results = new List<EvidenceBundleDetails>();
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
while (await reader.ReadAsync(cancellationToken))
// Use raw SQL for cursor-based pagination with composite (updated_at, bundle_id) ordering,
// as EF LINQ cannot express tuple comparisons directly.
var sinceUtc = since?.UtcDateTime;
var cursorUtc = cursorUpdatedAt?.UtcDateTime;
var cursorId = cursorBundleId?.Value;
var statusSealed = (short)EvidenceBundleStatus.Sealed;
var bundleEntities = await dbContext.EvidenceBundles
.FromSqlRaw("""
SELECT b.bundle_id, b.tenant_id, b.kind, b.status, b.root_hash, b.storage_key, b.description,
b.sealed_at, b.created_at, b.updated_at, b.expires_at, b.portable_storage_key, b.portable_generated_at
FROM evidence_locker.evidence_bundles b
WHERE b.tenant_id = {0}
AND b.status = {1}
AND ({2} IS NULL OR b.updated_at >= {2})
AND (
{3} IS NULL OR
(b.updated_at, b.bundle_id) > ({3}, {4})
)
ORDER BY b.updated_at, b.bundle_id
LIMIT {5}
""",
tenantId.Value, statusSealed, sinceUtc, cursorUtc, cursorId ?? Guid.Empty, limit)
.AsNoTracking()
.ToListAsync(cancellationToken);
if (bundleEntities.Count == 0)
{
results.Add(MapBundleDetails(reader));
return [];
}
var bundleIds = bundleEntities.Select(b => b.BundleId).ToList();
var signatures = await dbContext.EvidenceBundleSignatures
.AsNoTracking()
.Where(s => s.TenantId == tenantId.Value && bundleIds.Contains(s.BundleId))
.ToDictionaryAsync(s => s.BundleId, cancellationToken);
var results = new List<EvidenceBundleDetails>(bundleEntities.Count);
foreach (var entity in bundleEntities)
{
signatures.TryGetValue(entity.BundleId, out var sigEntity);
results.Add(MapBundleDetails(entity, sigEntity));
}
return results;
}
private static EvidenceBundleDetails MapBundleDetails(NpgsqlDataReader reader)
{
var bundleId = EvidenceBundleId.FromGuid(reader.GetGuid(0));
var tenantId = TenantId.FromGuid(reader.GetGuid(1));
var createdAt = new DateTimeOffset(DateTime.SpecifyKind(reader.GetDateTime(8), DateTimeKind.Utc));
var updatedAt = new DateTimeOffset(DateTime.SpecifyKind(reader.GetDateTime(9), DateTimeKind.Utc));
DateTimeOffset? sealedAt = null;
if (!reader.IsDBNull(7))
{
sealedAt = new DateTimeOffset(DateTime.SpecifyKind(reader.GetDateTime(7), DateTimeKind.Utc));
}
DateTimeOffset? expiresAt = null;
if (!reader.IsDBNull(10))
{
expiresAt = new DateTimeOffset(DateTime.SpecifyKind(reader.GetDateTime(10), DateTimeKind.Utc));
}
var portableStorageKey = reader.IsDBNull(11) ? null : reader.GetString(11);
DateTimeOffset? portableGeneratedAt = null;
if (!reader.IsDBNull(12))
{
portableGeneratedAt = new DateTimeOffset(DateTime.SpecifyKind(reader.GetDateTime(12), DateTimeKind.Utc));
}
EvidenceBundleSignature? signature = null;
if (!reader.IsDBNull(13))
{
var signedAt = new DateTimeOffset(DateTime.SpecifyKind(reader.GetDateTime(19), DateTimeKind.Utc));
DateTimeOffset? timestampedAt = null;
if (!reader.IsDBNull(20))
{
timestampedAt = new DateTimeOffset(DateTime.SpecifyKind(reader.GetDateTime(20), DateTimeKind.Utc));
}
byte[]? timestampToken = null;
if (!reader.IsDBNull(22))
{
timestampToken = (byte[])reader[22];
}
signature = new EvidenceBundleSignature(
bundleId,
tenantId,
reader.GetString(13),
reader.GetString(14),
reader.GetString(15),
reader.IsDBNull(16) ? null : reader.GetString(16),
reader.GetString(17),
reader.GetString(18),
signedAt,
timestampedAt,
reader.IsDBNull(21) ? null : reader.GetString(21),
timestampToken);
}
var bundle = new EvidenceBundle(
bundleId,
tenantId,
(EvidenceBundleKind)reader.GetInt16(2),
(EvidenceBundleStatus)reader.GetInt16(3),
reader.GetString(4),
reader.GetString(5),
createdAt,
updatedAt,
reader.IsDBNull(6) ? null : reader.GetString(6),
sealedAt,
expiresAt,
portableStorageKey,
portableGeneratedAt);
return new EvidenceBundleDetails(bundle, signature);
}
public async Task<bool> ExistsAsync(EvidenceBundleId bundleId, TenantId tenantId, CancellationToken cancellationToken)
{
await using var connection = await dataSource.OpenConnectionAsync(tenantId, cancellationToken);
await using var command = new NpgsqlCommand(ExistsSql, connection);
command.Parameters.AddWithValue("bundle_id", bundleId.Value);
command.Parameters.AddWithValue("tenant_id", tenantId.Value);
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
return await reader.ReadAsync(cancellationToken);
await using var dbContext = EvidenceLockerDbContextFactory.Create(connection, CommandTimeoutSeconds, EvidenceLockerDbContextFactory.DefaultSchemaName);
return await dbContext.EvidenceBundles
.AsNoTracking()
.AnyAsync(b => b.BundleId == bundleId.Value && b.TenantId == tenantId.Value, cancellationToken);
}
public async Task<EvidenceHold> CreateHoldAsync(EvidenceHold hold, CancellationToken cancellationToken)
{
await using var connection = await dataSource.OpenConnectionAsync(hold.TenantId, cancellationToken);
await using var command = new NpgsqlCommand(InsertHoldSql, connection);
command.Parameters.AddWithValue("hold_id", hold.Id.Value);
command.Parameters.AddWithValue("tenant_id", hold.TenantId.Value);
command.Parameters.AddWithValue("bundle_id", hold.BundleId?.Value ?? (object)DBNull.Value);
command.Parameters.AddWithValue("case_id", hold.CaseId);
command.Parameters.AddWithValue("reason", hold.Reason);
command.Parameters.AddWithValue("notes", hold.Notes ?? (object)DBNull.Value);
command.Parameters.AddWithValue("created_at", hold.CreatedAt.UtcDateTime);
command.Parameters.AddWithValue("expires_at", hold.ExpiresAt?.UtcDateTime ?? (object)DBNull.Value);
await using var dbContext = EvidenceLockerDbContextFactory.Create(connection, CommandTimeoutSeconds, EvidenceLockerDbContextFactory.DefaultSchemaName);
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
await reader.ReadAsync(cancellationToken);
var holdId = EvidenceHoldId.FromGuid(reader.GetGuid(0));
var tenantId = TenantId.FromGuid(reader.GetGuid(1));
EvidenceBundleId? bundleId = null;
if (!reader.IsDBNull(2))
var entity = new EvidenceHoldEntity
{
bundleId = EvidenceBundleId.FromGuid(reader.GetGuid(2));
}
HoldId = hold.Id.Value,
TenantId = hold.TenantId.Value,
BundleId = hold.BundleId?.Value,
CaseId = hold.CaseId,
Reason = hold.Reason,
Notes = hold.Notes,
CreatedAt = hold.CreatedAt.UtcDateTime,
ExpiresAt = hold.ExpiresAt?.UtcDateTime,
};
var createdAt = new DateTimeOffset(DateTime.SpecifyKind(reader.GetDateTime(6), DateTimeKind.Utc));
DateTimeOffset? expiresAt = null;
if (!reader.IsDBNull(7))
{
expiresAt = new DateTimeOffset(DateTime.SpecifyKind(reader.GetDateTime(7), DateTimeKind.Utc));
}
dbContext.EvidenceHolds.Add(entity);
await dbContext.SaveChangesAsync(cancellationToken);
DateTimeOffset? releasedAt = null;
if (!reader.IsDBNull(8))
{
releasedAt = new DateTimeOffset(DateTime.SpecifyKind(reader.GetDateTime(8), DateTimeKind.Utc));
}
// Re-read to get any DB-generated values
var saved = await dbContext.EvidenceHolds
.AsNoTracking()
.FirstAsync(h => h.HoldId == entity.HoldId, cancellationToken);
return new EvidenceHold(
holdId,
tenantId,
bundleId,
reader.GetString(3),
reader.GetString(4),
createdAt,
expiresAt,
releasedAt,
reader.IsDBNull(5) ? null : reader.GetString(5));
return MapHold(saved);
}
public async Task ExtendBundleRetentionAsync(
@@ -396,13 +247,27 @@ internal sealed class EvidenceBundleRepository(EvidenceLockerDataSource dataSour
DateTimeOffset processedAt,
CancellationToken cancellationToken)
{
// Use raw SQL to preserve the CASE/GREATEST logic exactly as originally defined.
await using var connection = await dataSource.OpenConnectionAsync(tenantId, cancellationToken);
await using var command = new NpgsqlCommand(ExtendRetentionSql, connection);
command.Parameters.AddWithValue("bundle_id", bundleId.Value);
command.Parameters.AddWithValue("tenant_id", tenantId.Value);
command.Parameters.AddWithValue("processed_at", processedAt.UtcDateTime);
command.Parameters.AddWithValue("hold_expires_at", holdExpiresAt?.UtcDateTime ?? (object)DBNull.Value);
await command.ExecuteNonQueryAsync(cancellationToken);
await using var dbContext = EvidenceLockerDbContextFactory.Create(connection, CommandTimeoutSeconds, EvidenceLockerDbContextFactory.DefaultSchemaName);
await dbContext.Database.ExecuteSqlRawAsync("""
UPDATE evidence_locker.evidence_bundles
SET expires_at = CASE
WHEN {2} IS NULL THEN NULL
WHEN expires_at IS NULL THEN {2}
WHEN expires_at < {2} THEN {2}
ELSE expires_at
END,
updated_at = GREATEST(updated_at, {3})
WHERE bundle_id = {0}
AND tenant_id = {1}
""",
bundleId.Value,
tenantId.Value,
(object?)holdExpiresAt?.UtcDateTime ?? DBNull.Value,
processedAt.UtcDateTime,
cancellationToken);
}
public async Task UpdateStorageKeyAsync(
@@ -412,12 +277,14 @@ internal sealed class EvidenceBundleRepository(EvidenceLockerDataSource dataSour
CancellationToken cancellationToken)
{
await using var connection = await dataSource.OpenConnectionAsync(tenantId, cancellationToken);
await using var command = new NpgsqlCommand(UpdateStorageKeySql, connection);
command.Parameters.AddWithValue("bundle_id", bundleId.Value);
command.Parameters.AddWithValue("tenant_id", tenantId.Value);
command.Parameters.AddWithValue("storage_key", storageKey);
await using var dbContext = EvidenceLockerDbContextFactory.Create(connection, CommandTimeoutSeconds, EvidenceLockerDbContextFactory.DefaultSchemaName);
await command.ExecuteNonQueryAsync(cancellationToken);
await dbContext.EvidenceBundles
.Where(b => b.BundleId == bundleId.Value && b.TenantId == tenantId.Value)
.ExecuteUpdateAsync(setters => setters
.SetProperty(b => b.StorageKey, storageKey)
.SetProperty(b => b.UpdatedAt, DateTime.UtcNow),
cancellationToken);
}
public async Task UpdatePortableStorageKeyAsync(
@@ -427,13 +294,109 @@ internal sealed class EvidenceBundleRepository(EvidenceLockerDataSource dataSour
DateTimeOffset generatedAt,
CancellationToken cancellationToken)
{
// Use raw SQL to preserve GREATEST semantics for updated_at.
await using var connection = await dataSource.OpenConnectionAsync(tenantId, cancellationToken);
await using var command = new NpgsqlCommand(UpdatePortableStorageKeySql, connection);
command.Parameters.AddWithValue("bundle_id", bundleId.Value);
command.Parameters.AddWithValue("tenant_id", tenantId.Value);
command.Parameters.AddWithValue("storage_key", storageKey);
command.Parameters.AddWithValue("generated_at", generatedAt.UtcDateTime);
await using var dbContext = EvidenceLockerDbContextFactory.Create(connection, CommandTimeoutSeconds, EvidenceLockerDbContextFactory.DefaultSchemaName);
await command.ExecuteNonQueryAsync(cancellationToken);
await dbContext.Database.ExecuteSqlRawAsync("""
UPDATE evidence_locker.evidence_bundles
SET portable_storage_key = {2},
portable_generated_at = {3},
updated_at = GREATEST(updated_at, {3})
WHERE bundle_id = {0}
AND tenant_id = {1}
""",
bundleId.Value,
tenantId.Value,
storageKey,
generatedAt.UtcDateTime,
cancellationToken);
}
private static EvidenceBundleDetails MapBundleDetails(EvidenceBundleEntity entity, EvidenceBundleSignatureEntity? sigEntity)
{
var bundleId = EvidenceBundleId.FromGuid(entity.BundleId);
var tenantId = TenantId.FromGuid(entity.TenantId);
var createdAt = new DateTimeOffset(DateTime.SpecifyKind(entity.CreatedAt, DateTimeKind.Utc));
var updatedAt = new DateTimeOffset(DateTime.SpecifyKind(entity.UpdatedAt, DateTimeKind.Utc));
DateTimeOffset? sealedAt = entity.SealedAt.HasValue
? new DateTimeOffset(DateTime.SpecifyKind(entity.SealedAt.Value, DateTimeKind.Utc))
: null;
DateTimeOffset? expiresAt = entity.ExpiresAt.HasValue
? new DateTimeOffset(DateTime.SpecifyKind(entity.ExpiresAt.Value, DateTimeKind.Utc))
: null;
DateTimeOffset? portableGeneratedAt = entity.PortableGeneratedAt.HasValue
? new DateTimeOffset(DateTime.SpecifyKind(entity.PortableGeneratedAt.Value, DateTimeKind.Utc))
: null;
var bundle = new EvidenceBundle(
bundleId,
tenantId,
(EvidenceBundleKind)entity.Kind,
(EvidenceBundleStatus)entity.Status,
entity.RootHash,
entity.StorageKey,
createdAt,
updatedAt,
entity.Description,
sealedAt,
expiresAt,
entity.PortableStorageKey,
portableGeneratedAt);
EvidenceBundleSignature? signature = null;
if (sigEntity is not null)
{
var signedAt = new DateTimeOffset(DateTime.SpecifyKind(sigEntity.SignedAt, DateTimeKind.Utc));
DateTimeOffset? timestampedAt = sigEntity.TimestampedAt.HasValue
? new DateTimeOffset(DateTime.SpecifyKind(sigEntity.TimestampedAt.Value, DateTimeKind.Utc))
: null;
signature = new EvidenceBundleSignature(
bundleId,
tenantId,
sigEntity.PayloadType,
sigEntity.Payload,
sigEntity.Signature,
sigEntity.KeyId,
sigEntity.Algorithm,
sigEntity.Provider,
signedAt,
timestampedAt,
sigEntity.TimestampAuthority,
sigEntity.TimestampToken);
}
return new EvidenceBundleDetails(bundle, signature);
}
private static EvidenceHold MapHold(EvidenceHoldEntity entity)
{
var holdId = EvidenceHoldId.FromGuid(entity.HoldId);
var tenantId = TenantId.FromGuid(entity.TenantId);
EvidenceBundleId? bundleId = entity.BundleId.HasValue
? EvidenceBundleId.FromGuid(entity.BundleId.Value)
: null;
var createdAt = new DateTimeOffset(DateTime.SpecifyKind(entity.CreatedAt, DateTimeKind.Utc));
DateTimeOffset? expiresAt = entity.ExpiresAt.HasValue
? new DateTimeOffset(DateTime.SpecifyKind(entity.ExpiresAt.Value, DateTimeKind.Utc))
: null;
DateTimeOffset? releasedAt = entity.ReleasedAt.HasValue
? new DateTimeOffset(DateTime.SpecifyKind(entity.ReleasedAt.Value, DateTimeKind.Utc))
: null;
return new EvidenceHold(
holdId,
tenantId,
bundleId,
entity.CaseId,
entity.Reason,
createdAt,
expiresAt,
releasedAt,
entity.Notes);
}
}

View File

@@ -1,42 +1,15 @@
using Npgsql;
using NpgsqlTypes;
using Microsoft.EntityFrameworkCore;
using StellaOps.EvidenceLocker.Core.Domain;
using StellaOps.EvidenceLocker.Core.Repositories;
using StellaOps.EvidenceLocker.Infrastructure.Db;
using StellaOps.EvidenceLocker.Infrastructure.EfCore.Models;
using System.Text.Json;
namespace StellaOps.EvidenceLocker.Infrastructure.Repositories;
internal sealed class EvidenceGateArtifactRepository(EvidenceLockerDataSource dataSource) : IEvidenceGateArtifactRepository
{
private const string UpsertSql = """
INSERT INTO evidence_locker.evidence_gate_artifacts
(evidence_id, tenant_id, artifact_id, canonical_bom_sha256, payload_digest, dsse_envelope_ref, rekor_index, rekor_tile_id, rekor_inclusion_proof_ref, attestation_refs, raw_bom_ref, vex_refs, evidence_score, created_at, updated_at)
VALUES
(@evidence_id, @tenant_id, @artifact_id, @canonical_bom_sha256, @payload_digest, @dsse_envelope_ref, @rekor_index, @rekor_tile_id, @rekor_inclusion_proof_ref, @attestation_refs, @raw_bom_ref, @vex_refs, @evidence_score, @created_at, @updated_at)
ON CONFLICT (tenant_id, artifact_id)
DO UPDATE SET
evidence_id = EXCLUDED.evidence_id,
canonical_bom_sha256 = EXCLUDED.canonical_bom_sha256,
payload_digest = EXCLUDED.payload_digest,
dsse_envelope_ref = EXCLUDED.dsse_envelope_ref,
rekor_index = EXCLUDED.rekor_index,
rekor_tile_id = EXCLUDED.rekor_tile_id,
rekor_inclusion_proof_ref = EXCLUDED.rekor_inclusion_proof_ref,
attestation_refs = EXCLUDED.attestation_refs,
raw_bom_ref = EXCLUDED.raw_bom_ref,
vex_refs = EXCLUDED.vex_refs,
evidence_score = EXCLUDED.evidence_score,
updated_at = EXCLUDED.updated_at
RETURNING evidence_id, tenant_id, artifact_id, canonical_bom_sha256, payload_digest, dsse_envelope_ref, rekor_index, rekor_tile_id, rekor_inclusion_proof_ref, attestation_refs, raw_bom_ref, vex_refs, evidence_score, created_at, updated_at;
""";
private const string SelectByArtifactSql = """
SELECT evidence_id, tenant_id, artifact_id, canonical_bom_sha256, payload_digest, dsse_envelope_ref, rekor_index, rekor_tile_id, rekor_inclusion_proof_ref, attestation_refs, raw_bom_ref, vex_refs, evidence_score, created_at, updated_at
FROM evidence_locker.evidence_gate_artifacts
WHERE tenant_id = @tenant_id
AND artifact_id = @artifact_id;
""";
private const int CommandTimeoutSeconds = 30;
public async Task<EvidenceGateArtifactRecord> UpsertAsync(
EvidenceGateArtifactRecord record,
@@ -45,30 +18,55 @@ internal sealed class EvidenceGateArtifactRepository(EvidenceLockerDataSource da
ArgumentNullException.ThrowIfNull(record);
await using var connection = await dataSource.OpenConnectionAsync(record.TenantId, cancellationToken);
await using var command = new NpgsqlCommand(UpsertSql, connection);
command.Parameters.AddWithValue("evidence_id", record.EvidenceId);
command.Parameters.AddWithValue("tenant_id", record.TenantId.Value);
command.Parameters.AddWithValue("artifact_id", record.ArtifactId);
command.Parameters.AddWithValue("canonical_bom_sha256", record.CanonicalBomSha256);
command.Parameters.AddWithValue("payload_digest", record.PayloadDigest);
command.Parameters.AddWithValue("dsse_envelope_ref", record.DsseEnvelopeRef);
command.Parameters.AddWithValue("rekor_index", record.RekorIndex);
command.Parameters.AddWithValue("rekor_tile_id", record.RekorTileId);
command.Parameters.AddWithValue("rekor_inclusion_proof_ref", record.RekorInclusionProofRef);
command.Parameters.AddWithValue("raw_bom_ref", (object?)record.RawBomRef ?? DBNull.Value);
command.Parameters.AddWithValue("evidence_score", record.EvidenceScore);
command.Parameters.AddWithValue("created_at", record.CreatedAt.UtcDateTime);
command.Parameters.AddWithValue("updated_at", record.UpdatedAt.UtcDateTime);
await using var dbContext = EvidenceLockerDbContextFactory.Create(connection, CommandTimeoutSeconds, EvidenceLockerDbContextFactory.DefaultSchemaName);
var attestationParameter = command.Parameters.Add("attestation_refs", NpgsqlDbType.Jsonb);
attestationParameter.Value = JsonSerializer.Serialize(record.AttestationRefs);
// Use raw SQL for UPSERT with ON CONFLICT on composite key (tenant_id, artifact_id)
// and RETURNING clause, which is more natural in SQL than EF's catch-and-update pattern.
var attestationRefsJson = JsonSerializer.Serialize(record.AttestationRefs);
var vexRefsJson = JsonSerializer.Serialize(record.VexRefs);
var vexParameter = command.Parameters.Add("vex_refs", NpgsqlDbType.Jsonb);
vexParameter.Value = JsonSerializer.Serialize(record.VexRefs);
var entities = await dbContext.EvidenceGateArtifacts
.FromSqlRaw("""
INSERT INTO evidence_locker.evidence_gate_artifacts
(evidence_id, tenant_id, artifact_id, canonical_bom_sha256, payload_digest, dsse_envelope_ref, rekor_index, rekor_tile_id, rekor_inclusion_proof_ref, attestation_refs, raw_bom_ref, vex_refs, evidence_score, created_at, updated_at)
VALUES
({0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}::jsonb, {10}, {11}::jsonb, {12}, {13}, {14})
ON CONFLICT (tenant_id, artifact_id)
DO UPDATE SET
evidence_id = EXCLUDED.evidence_id,
canonical_bom_sha256 = EXCLUDED.canonical_bom_sha256,
payload_digest = EXCLUDED.payload_digest,
dsse_envelope_ref = EXCLUDED.dsse_envelope_ref,
rekor_index = EXCLUDED.rekor_index,
rekor_tile_id = EXCLUDED.rekor_tile_id,
rekor_inclusion_proof_ref = EXCLUDED.rekor_inclusion_proof_ref,
attestation_refs = EXCLUDED.attestation_refs,
raw_bom_ref = EXCLUDED.raw_bom_ref,
vex_refs = EXCLUDED.vex_refs,
evidence_score = EXCLUDED.evidence_score,
updated_at = EXCLUDED.updated_at
RETURNING evidence_id, tenant_id, artifact_id, canonical_bom_sha256, payload_digest, dsse_envelope_ref, rekor_index, rekor_tile_id, rekor_inclusion_proof_ref, attestation_refs, raw_bom_ref, vex_refs, evidence_score, created_at, updated_at
""",
record.EvidenceId,
record.TenantId.Value,
record.ArtifactId,
record.CanonicalBomSha256,
record.PayloadDigest,
record.DsseEnvelopeRef,
record.RekorIndex,
record.RekorTileId,
record.RekorInclusionProofRef,
attestationRefsJson,
(object?)record.RawBomRef ?? DBNull.Value,
vexRefsJson,
record.EvidenceScore,
record.CreatedAt.UtcDateTime,
record.UpdatedAt.UtcDateTime)
.AsNoTracking()
.ToListAsync(cancellationToken);
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
await reader.ReadAsync(cancellationToken);
return MapRecord(reader);
var entity = entities.First();
return MapRecord(entity);
}
public async Task<EvidenceGateArtifactRecord?> GetByArtifactIdAsync(
@@ -77,42 +75,43 @@ internal sealed class EvidenceGateArtifactRepository(EvidenceLockerDataSource da
CancellationToken cancellationToken)
{
await using var connection = await dataSource.OpenConnectionAsync(tenantId, cancellationToken);
await using var command = new NpgsqlCommand(SelectByArtifactSql, connection);
command.Parameters.AddWithValue("tenant_id", tenantId.Value);
command.Parameters.AddWithValue("artifact_id", artifactId);
await using var dbContext = EvidenceLockerDbContextFactory.Create(connection, CommandTimeoutSeconds, EvidenceLockerDbContextFactory.DefaultSchemaName);
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
if (!await reader.ReadAsync(cancellationToken))
var entity = await dbContext.EvidenceGateArtifacts
.AsNoTracking()
.Where(e => e.TenantId == tenantId.Value && e.ArtifactId == artifactId)
.FirstOrDefaultAsync(cancellationToken);
if (entity is null)
{
return null;
}
return MapRecord(reader);
return MapRecord(entity);
}
private static EvidenceGateArtifactRecord MapRecord(NpgsqlDataReader reader)
private static EvidenceGateArtifactRecord MapRecord(EvidenceGateArtifactEntity entity)
{
var tenantId = TenantId.FromGuid(reader.GetGuid(1));
var attestationRefs = DeserializeStringArray(reader.GetString(9));
var rawBomRef = reader.IsDBNull(10) ? null : reader.GetString(10);
var vexRefs = DeserializeStringArray(reader.GetString(11));
var createdAt = new DateTimeOffset(DateTime.SpecifyKind(reader.GetDateTime(13), DateTimeKind.Utc));
var updatedAt = new DateTimeOffset(DateTime.SpecifyKind(reader.GetDateTime(14), DateTimeKind.Utc));
var tenantId = TenantId.FromGuid(entity.TenantId);
var attestationRefs = DeserializeStringArray(entity.AttestationRefs);
var vexRefs = DeserializeStringArray(entity.VexRefs);
var createdAt = new DateTimeOffset(DateTime.SpecifyKind(entity.CreatedAt, DateTimeKind.Utc));
var updatedAt = new DateTimeOffset(DateTime.SpecifyKind(entity.UpdatedAt, DateTimeKind.Utc));
return new EvidenceGateArtifactRecord(
reader.GetString(0),
entity.EvidenceId,
tenantId,
reader.GetString(2),
reader.GetString(3),
reader.GetString(4),
reader.GetString(5),
reader.GetInt64(6),
reader.GetString(7),
reader.GetString(8),
entity.ArtifactId,
entity.CanonicalBomSha256,
entity.PayloadDigest,
entity.DsseEnvelopeRef,
entity.RekorIndex,
entity.RekorTileId,
entity.RekorInclusionProofRef,
attestationRefs,
rawBomRef,
entity.RawBomRef,
vexRefs,
reader.GetString(12),
entity.EvidenceScore,
createdAt,
updatedAt);
}

View File

@@ -19,6 +19,8 @@
<ItemGroup>
<PackageReference Include="AWSSDK.S3" />
<PackageReference Include="Microsoft.EntityFrameworkCore" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
@@ -27,9 +29,15 @@
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" />
<PackageReference Include="Microsoft.Extensions.Options.DataAnnotations" />
<PackageReference Include="Npgsql" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Db\Migrations\*.sql" />
</ItemGroup>
<ItemGroup>
<!-- Prevent automatic compiled-model binding so non-default schemas can build runtime models. -->
<Compile Remove="EfCore\CompiledModels\EvidenceLockerDbContextAssemblyAttributes.cs" />
</ItemGroup>
</Project>

View File

@@ -10,3 +10,8 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| AUDIT-0289-A | TODO | Revalidated 2026-01-07 (open findings). |
| EL-GATE-002 | DONE | Added `evidence_gate_artifacts` persistence, migration `004_gate_artifacts.sql`, and repository/service wiring (2026-02-09). |
| PAPI-001 | DONE | SPRINT_20260210_005 - Portable audit pack v1 writer/schema wiring in EvidencePortableBundleService (2026-02-10); deterministic portable profile and manifest parity validated by module tests. |
| EVLOCK-EF-01 | DONE | Verified AGENTS.md, added EvidenceLockerMigrationModulePlugin to Platform migration registry, added project reference (2026-02-23). |
| EVLOCK-EF-02 | DONE | Scaffolded EF Core model baseline: 6 entity models, DbContext with OnModelCreating, compiled model (9 files), design-time and runtime factories (2026-02-23). |
| EVLOCK-EF-03 | DONE | Converted EvidenceBundleRepository and EvidenceGateArtifactRepository from raw Npgsql to EF Core v10. Raw SQL retained for UPSERT ON CONFLICT, cursor pagination, and GREATEST/CASE expressions (2026-02-23). |
| EVLOCK-EF-04 | DONE | Verified compiled model artifacts (6 entity types + model + builder + assembly attributes), runtime UseModel on default schema, non-default schema bypass. Build 0 errors (2026-02-23). |
| EVLOCK-EF-05 | DONE | Sequential builds pass (0 warnings, 0 errors). AGENTS.md, TASKS.md, and sprint updated (2026-02-23). |

View File

@@ -0,0 +1,213 @@
// -----------------------------------------------------------------------------
// TenantIsolationTests.cs
// Description: Tenant isolation unit tests for EvidenceLocker module.
// Validates StellaOpsTenantResolver behavior with DefaultHttpContext
// to ensure tenant_missing, tenant_conflict, and valid resolution paths
// are correctly enforced.
// -----------------------------------------------------------------------------
using FluentAssertions;
using Microsoft.AspNetCore.Http;
using StellaOps.Auth.Abstractions;
using StellaOps.Auth.ServerIntegration.Tenancy;
using System.Security.Claims;
namespace StellaOps.EvidenceLocker.Tests;
[Trait("Category", "Unit")]
public sealed class TenantIsolationTests
{
// ── 1. Missing tenant returns error ──────────────────────────────────
[Fact]
public void TryResolveTenantId_WithNoClaims_AndNoHeaders_ReturnsFalse_WithTenantMissing()
{
// Arrange: bare context -- no claims, no headers
var context = new DefaultHttpContext();
context.User = new ClaimsPrincipal(new ClaimsIdentity());
// Act
var result = StellaOpsTenantResolver.TryResolveTenantId(context, out var tenantId, out var error);
// Assert
result.Should().BeFalse("no tenant claim or header was provided");
tenantId.Should().BeEmpty();
error.Should().Be("tenant_missing");
}
[Fact]
public void TryResolve_WithNoClaims_AndNoHeaders_ReturnsFalse_WithTenantMissing()
{
// Arrange
var context = new DefaultHttpContext();
context.User = new ClaimsPrincipal(new ClaimsIdentity());
// Act
var result = StellaOpsTenantResolver.TryResolve(context, out var tenantContext, out var error);
// Assert
result.Should().BeFalse("no tenant claim or header was provided");
tenantContext.Should().BeNull();
error.Should().Be("tenant_missing");
}
// ── 2. Valid tenant via canonical claim succeeds ─────────────────────
[Fact]
public void TryResolveTenantId_WithCanonicalClaim_ReturnsTenantId()
{
// Arrange
var context = new DefaultHttpContext();
context.User = new ClaimsPrincipal(
new ClaimsIdentity(
new[] { new Claim(StellaOpsClaimTypes.Tenant, "evidence-tenant-a") },
authenticationType: "test"));
// Act
var result = StellaOpsTenantResolver.TryResolveTenantId(context, out var tenantId, out var error);
// Assert
result.Should().BeTrue();
tenantId.Should().Be("evidence-tenant-a");
error.Should().BeNull();
}
[Fact]
public void TryResolve_WithCanonicalClaim_ReturnsTenantContext_WithClaimSource()
{
// Arrange
var context = new DefaultHttpContext();
context.User = new ClaimsPrincipal(
new ClaimsIdentity(
new[]
{
new Claim(StellaOpsClaimTypes.Tenant, "EVIDENCE-TENANT-B"),
new Claim(StellaOpsClaimTypes.Subject, "auditor-7"),
},
authenticationType: "test"));
// Act
var result = StellaOpsTenantResolver.TryResolve(context, out var tenantContext, out var error);
// Assert
result.Should().BeTrue();
error.Should().BeNull();
tenantContext.Should().NotBeNull();
tenantContext!.TenantId.Should().Be("evidence-tenant-b", "tenant IDs are normalised to lower-case");
tenantContext.Source.Should().Be(TenantSource.Claim);
tenantContext.ActorId.Should().Be("auditor-7");
}
[Fact]
public void TryResolveTenantId_WithCanonicalHeader_ReturnsTenantId()
{
// Arrange
var context = new DefaultHttpContext();
context.User = new ClaimsPrincipal(new ClaimsIdentity());
context.Request.Headers[StellaOpsHttpHeaderNames.Tenant] = "evidence-header-tenant";
// Act
var result = StellaOpsTenantResolver.TryResolveTenantId(context, out var tenantId, out var error);
// Assert
result.Should().BeTrue();
tenantId.Should().Be("evidence-header-tenant");
error.Should().BeNull();
}
[Fact]
public void TryResolveTenantId_WithLegacyTidClaim_ReturnsTenantId()
{
// Arrange: legacy "tid" claim should also resolve
var context = new DefaultHttpContext();
context.User = new ClaimsPrincipal(
new ClaimsIdentity(
new[] { new Claim("tid", "legacy-evidence-tenant") },
authenticationType: "test"));
// Act
var result = StellaOpsTenantResolver.TryResolveTenantId(context, out var tenantId, out var error);
// Assert
result.Should().BeTrue();
tenantId.Should().Be("legacy-evidence-tenant");
error.Should().BeNull();
}
// ── 3. Conflicting headers return tenant_conflict ───────────────────
[Fact]
public void TryResolveTenantId_WithConflictingHeaders_ReturnsFalse_WithTenantConflict()
{
// Arrange: canonical X-StellaOps-Tenant and legacy X-Stella-Tenant have different values
var context = new DefaultHttpContext();
context.User = new ClaimsPrincipal(new ClaimsIdentity());
context.Request.Headers[StellaOpsHttpHeaderNames.Tenant] = "evidence-alpha";
context.Request.Headers["X-Stella-Tenant"] = "evidence-beta";
// Act
var result = StellaOpsTenantResolver.TryResolveTenantId(context, out var tenantId, out var error);
// Assert
result.Should().BeFalse("conflicting headers should be rejected");
error.Should().Be("tenant_conflict");
}
[Fact]
public void TryResolveTenantId_WithConflictingCanonicalAndAlternateHeaders_ReturnsFalse_WithTenantConflict()
{
// Arrange: canonical X-StellaOps-Tenant and alternate X-Tenant-Id have different values
var context = new DefaultHttpContext();
context.User = new ClaimsPrincipal(new ClaimsIdentity());
context.Request.Headers[StellaOpsHttpHeaderNames.Tenant] = "evidence-one";
context.Request.Headers["X-Tenant-Id"] = "evidence-two";
// Act
var result = StellaOpsTenantResolver.TryResolveTenantId(context, out var tenantId, out var error);
// Assert
result.Should().BeFalse("conflicting headers should be rejected");
error.Should().Be("tenant_conflict");
}
[Fact]
public void TryResolveTenantId_WithClaimHeaderMismatch_ReturnsFalse_WithTenantConflict()
{
// Arrange: claim says "evidence-claim" but header says "evidence-header" -- mismatch
var context = new DefaultHttpContext();
context.User = new ClaimsPrincipal(
new ClaimsIdentity(
new[] { new Claim(StellaOpsClaimTypes.Tenant, "evidence-claim") },
authenticationType: "test"));
context.Request.Headers[StellaOpsHttpHeaderNames.Tenant] = "evidence-header";
// Act
var result = StellaOpsTenantResolver.TryResolveTenantId(context, out var tenantId, out var error);
// Assert
result.Should().BeFalse("claim-header mismatch is a conflict");
error.Should().Be("tenant_conflict");
}
// ── 4. Matching claim + header is not a conflict ────────────────────
[Fact]
public void TryResolveTenantId_WithMatchingClaimAndHeader_ReturnsTrue()
{
// Arrange: claim and header agree on the same tenant
var context = new DefaultHttpContext();
context.User = new ClaimsPrincipal(
new ClaimsIdentity(
new[] { new Claim(StellaOpsClaimTypes.Tenant, "evidence-same") },
authenticationType: "test"));
context.Request.Headers[StellaOpsHttpHeaderNames.Tenant] = "evidence-same";
// Act
var result = StellaOpsTenantResolver.TryResolveTenantId(context, out var tenantId, out var error);
// Assert
result.Should().BeTrue("claim and header agree");
tenantId.Should().Be("evidence-same");
error.Should().BeNull();
}
}

View File

@@ -9,6 +9,7 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using StellaOps.Auth.Abstractions;
using StellaOps.Auth.ServerIntegration;
using StellaOps.Auth.ServerIntegration.Tenancy;
using StellaOps.EvidenceLocker.Api;
using StellaOps.EvidenceLocker.Core.Domain;
using StellaOps.EvidenceLocker.Core.Storage;
@@ -43,6 +44,8 @@ builder.Services.AddAuthorization(options =>
options.FallbackPolicy = options.DefaultPolicy;
});
builder.Services.AddStellaOpsTenantServices();
builder.Services.AddOpenApi();
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
@@ -66,20 +69,16 @@ if (app.Environment.IsDevelopment())
app.UseStellaOpsCors();
app.UseAuthentication();
app.UseAuthorization();
app.UseStellaOpsTenantMiddleware();
app.TryUseStellaRouter(routerEnabled);
app.MapHealthChecks("/health/ready");
app.MapPost("/evidence",
async (HttpContext context, ClaimsPrincipal user, EvidenceGateArtifactRequestDto request, EvidenceGateArtifactService service, ILoggerFactory loggerFactory, CancellationToken cancellationToken) =>
async (HttpContext context, ClaimsPrincipal user, IStellaOpsTenantAccessor tenantAccessor, EvidenceGateArtifactRequestDto request, EvidenceGateArtifactService service, ILoggerFactory loggerFactory, CancellationToken cancellationToken) =>
{
var logger = loggerFactory.CreateLogger(EvidenceAuditLogger.LoggerName);
if (!TenantResolution.TryResolveTenant(user, out var tenantId))
{
EvidenceAuditLogger.LogTenantMissing(logger, user, context.Request.Path.Value ?? "/evidence");
return ForbidTenant();
}
var tenantId = TenantId.FromGuid(Guid.Parse(tenantAccessor.TenantId!));
try
{
@@ -96,6 +95,7 @@ app.MapPost("/evidence",
}
})
.RequireAuthorization(StellaOpsResourceServerPolicies.EvidenceCreate)
.RequireTenant()
.Produces<EvidenceGateArtifactResponseDto>(StatusCodes.Status201Created)
.Produces<ErrorResponse>(StatusCodes.Status400BadRequest)
.Produces<ErrorResponse>(StatusCodes.Status403Forbidden)
@@ -104,15 +104,10 @@ app.MapPost("/evidence",
.WithSummary("Ingest producer gate artifact evidence and compute deterministic evidence score.");
app.MapGet("/evidence/score",
async (HttpContext context, ClaimsPrincipal user, string artifact_id, EvidenceGateArtifactService service, ILoggerFactory loggerFactory, CancellationToken cancellationToken) =>
async (HttpContext context, ClaimsPrincipal user, IStellaOpsTenantAccessor tenantAccessor, string artifact_id, EvidenceGateArtifactService service, ILoggerFactory loggerFactory, CancellationToken cancellationToken) =>
{
var logger = loggerFactory.CreateLogger(EvidenceAuditLogger.LoggerName);
if (!TenantResolution.TryResolveTenant(user, out var tenantId))
{
EvidenceAuditLogger.LogTenantMissing(logger, user, context.Request.Path.Value ?? "/evidence/score");
return ForbidTenant();
}
var tenantId = TenantId.FromGuid(Guid.Parse(tenantAccessor.TenantId!));
try
{
@@ -133,6 +128,7 @@ app.MapGet("/evidence/score",
}
})
.RequireAuthorization(StellaOpsResourceServerPolicies.EvidenceRead)
.RequireTenant()
.Produces<EvidenceScoreResponseDto>(StatusCodes.Status200OK)
.Produces<ErrorResponse>(StatusCodes.Status400BadRequest)
.Produces<ErrorResponse>(StatusCodes.Status403Forbidden)
@@ -142,15 +138,10 @@ app.MapGet("/evidence/score",
.WithSummary("Get deterministic evidence score by artifact identifier.");
app.MapPost("/evidence/snapshot",
async (HttpContext context, ClaimsPrincipal user, EvidenceSnapshotRequestDto request, EvidenceSnapshotService service, ILoggerFactory loggerFactory, CancellationToken cancellationToken) =>
async (HttpContext context, ClaimsPrincipal user, IStellaOpsTenantAccessor tenantAccessor, EvidenceSnapshotRequestDto request, EvidenceSnapshotService service, ILoggerFactory loggerFactory, CancellationToken cancellationToken) =>
{
var logger = loggerFactory.CreateLogger(EvidenceAuditLogger.LoggerName);
if (!TenantResolution.TryResolveTenant(user, out var tenantId))
{
EvidenceAuditLogger.LogTenantMissing(logger, user, context.Request.Path.Value ?? "/evidence/snapshot");
return ForbidTenant();
}
var tenantId = TenantId.FromGuid(Guid.Parse(tenantAccessor.TenantId!));
try
{
@@ -173,6 +164,7 @@ app.MapPost("/evidence/snapshot",
}
})
.RequireAuthorization(StellaOpsResourceServerPolicies.EvidenceHold)
.RequireTenant()
.Produces<EvidenceSnapshotResponseDto>(StatusCodes.Status201Created)
.Produces<ErrorResponse>(StatusCodes.Status400BadRequest)
.Produces<ErrorResponse>(StatusCodes.Status403Forbidden)
@@ -181,15 +173,10 @@ app.MapPost("/evidence/snapshot",
.WithSummary("Create a new evidence snapshot for the tenant.");
app.MapGet("/evidence/{bundleId:guid}",
async (HttpContext context, ClaimsPrincipal user, Guid bundleId, EvidenceSnapshotService service, ILoggerFactory loggerFactory, CancellationToken cancellationToken) =>
async (HttpContext context, ClaimsPrincipal user, IStellaOpsTenantAccessor tenantAccessor, Guid bundleId, EvidenceSnapshotService service, ILoggerFactory loggerFactory, CancellationToken cancellationToken) =>
{
var logger = loggerFactory.CreateLogger(EvidenceAuditLogger.LoggerName);
if (!TenantResolution.TryResolveTenant(user, out var tenantId))
{
EvidenceAuditLogger.LogTenantMissing(logger, user, context.Request.Path.Value ?? "/evidence/{bundleId}");
return ForbidTenant();
}
var tenantId = TenantId.FromGuid(Guid.Parse(tenantAccessor.TenantId!));
var details = await service.GetBundleAsync(tenantId, EvidenceBundleId.FromGuid(bundleId), cancellationToken);
if (details is null)
@@ -241,6 +228,7 @@ app.MapGet("/evidence/{bundleId:guid}",
return Results.Ok(dto);
})
.RequireAuthorization(StellaOpsResourceServerPolicies.EvidenceRead)
.RequireTenant()
.Produces<EvidenceBundleResponseDto>(StatusCodes.Status200OK)
.Produces<ErrorResponse>(StatusCodes.Status403Forbidden)
.Produces<ErrorResponse>(StatusCodes.Status404NotFound)
@@ -250,6 +238,7 @@ app.MapGet("/evidence/{bundleId:guid}",
app.MapGet("/evidence/{bundleId:guid}/download",
async (HttpContext context,
ClaimsPrincipal user,
IStellaOpsTenantAccessor tenantAccessor,
Guid bundleId,
EvidenceSnapshotService snapshotService,
EvidenceBundlePackagingService packagingService,
@@ -258,12 +247,7 @@ app.MapGet("/evidence/{bundleId:guid}/download",
CancellationToken cancellationToken) =>
{
var logger = loggerFactory.CreateLogger(EvidenceAuditLogger.LoggerName);
if (!TenantResolution.TryResolveTenant(user, out var tenantId))
{
EvidenceAuditLogger.LogTenantMissing(logger, user, context.Request.Path.Value ?? "/evidence/{bundleId}/download");
return ForbidTenant();
}
var tenantId = TenantId.FromGuid(Guid.Parse(tenantAccessor.TenantId!));
var bundle = await snapshotService.GetBundleAsync(tenantId, EvidenceBundleId.FromGuid(bundleId), cancellationToken);
if (bundle is null)
@@ -290,6 +274,7 @@ app.MapGet("/evidence/{bundleId:guid}/download",
}
})
.RequireAuthorization(StellaOpsResourceServerPolicies.EvidenceRead)
.RequireTenant()
.Produces(StatusCodes.Status200OK)
.Produces<ErrorResponse>(StatusCodes.Status400BadRequest)
.Produces<ErrorResponse>(StatusCodes.Status403Forbidden)
@@ -300,6 +285,7 @@ app.MapGet("/evidence/{bundleId:guid}/download",
app.MapGet("/evidence/{bundleId:guid}/portable",
async (HttpContext context,
ClaimsPrincipal user,
IStellaOpsTenantAccessor tenantAccessor,
Guid bundleId,
EvidenceSnapshotService snapshotService,
EvidencePortableBundleService portableService,
@@ -308,12 +294,7 @@ app.MapGet("/evidence/{bundleId:guid}/portable",
CancellationToken cancellationToken) =>
{
var logger = loggerFactory.CreateLogger(EvidenceAuditLogger.LoggerName);
if (!TenantResolution.TryResolveTenant(user, out var tenantId))
{
EvidenceAuditLogger.LogTenantMissing(logger, user, context.Request.Path.Value ?? "/evidence/{bundleId}/portable");
return ForbidTenant();
}
var tenantId = TenantId.FromGuid(Guid.Parse(tenantAccessor.TenantId!));
var bundle = await snapshotService.GetBundleAsync(tenantId, EvidenceBundleId.FromGuid(bundleId), cancellationToken);
if (bundle is null)
@@ -340,6 +321,7 @@ app.MapGet("/evidence/{bundleId:guid}/portable",
}
})
.RequireAuthorization(StellaOpsResourceServerPolicies.EvidenceRead)
.RequireTenant()
.Produces(StatusCodes.Status200OK)
.Produces<ErrorResponse>(StatusCodes.Status400BadRequest)
.Produces<ErrorResponse>(StatusCodes.Status403Forbidden)
@@ -349,15 +331,10 @@ app.MapGet("/evidence/{bundleId:guid}/portable",
.WithSummary("Download a sealed, portable evidence bundle for sealed or air-gapped distribution.");
app.MapPost("/evidence/verify",
async (HttpContext context, ClaimsPrincipal user, EvidenceVerifyRequestDto request, EvidenceSnapshotService service, ILoggerFactory loggerFactory, CancellationToken cancellationToken) =>
async (HttpContext context, ClaimsPrincipal user, IStellaOpsTenantAccessor tenantAccessor, EvidenceVerifyRequestDto request, EvidenceSnapshotService service, ILoggerFactory loggerFactory, CancellationToken cancellationToken) =>
{
var logger = loggerFactory.CreateLogger(EvidenceAuditLogger.LoggerName);
if (!TenantResolution.TryResolveTenant(user, out var tenantId))
{
EvidenceAuditLogger.LogTenantMissing(logger, user, context.Request.Path.Value ?? "/evidence/verify");
return ForbidTenant();
}
var tenantId = TenantId.FromGuid(Guid.Parse(tenantAccessor.TenantId!));
var trusted = await service.VerifyAsync(tenantId, EvidenceBundleId.FromGuid(request.BundleId), request.RootHash, cancellationToken);
EvidenceAuditLogger.LogVerificationResult(logger, user, tenantId, request.BundleId, request.RootHash, trusted);
@@ -365,13 +342,14 @@ app.MapPost("/evidence/verify",
return Results.Ok(new EvidenceVerifyResponseDto(trusted));
})
.RequireAuthorization(StellaOpsResourceServerPolicies.EvidenceRead)
.RequireTenant()
.Produces<EvidenceVerifyResponseDto>(StatusCodes.Status200OK)
.Produces<ErrorResponse>(StatusCodes.Status403Forbidden)
.WithName("VerifyEvidenceBundle")
.WithTags("Evidence");
app.MapPost("/evidence/hold/{caseId}",
async (HttpContext context, ClaimsPrincipal user, string caseId, EvidenceHoldRequestDto request, EvidenceSnapshotService service, ILoggerFactory loggerFactory, CancellationToken cancellationToken) =>
async (HttpContext context, ClaimsPrincipal user, IStellaOpsTenantAccessor tenantAccessor, string caseId, EvidenceHoldRequestDto request, EvidenceSnapshotService service, ILoggerFactory loggerFactory, CancellationToken cancellationToken) =>
{
var logger = loggerFactory.CreateLogger(EvidenceAuditLogger.LoggerName);
@@ -380,11 +358,7 @@ app.MapPost("/evidence/hold/{caseId}",
return ValidationProblem("Case identifier is required.");
}
if (!TenantResolution.TryResolveTenant(user, out var tenantId))
{
EvidenceAuditLogger.LogTenantMissing(logger, user, context.Request.Path.Value ?? "/evidence/hold/{caseId}");
return ForbidTenant();
}
var tenantId = TenantId.FromGuid(Guid.Parse(tenantAccessor.TenantId!));
try
{
@@ -427,6 +401,7 @@ app.MapPost("/evidence/hold/{caseId}",
}
})
.RequireAuthorization(StellaOpsResourceServerPolicies.EvidenceCreate)
.RequireTenant()
.Produces<EvidenceHoldResponseDto>(StatusCodes.Status201Created)
.Produces<ErrorResponse>(StatusCodes.Status400BadRequest)
.Produces<ErrorResponse>(StatusCodes.Status403Forbidden)
@@ -451,8 +426,6 @@ app.TryRefreshStellaRouterEndpoints(routerEnabled);
app.Run();
static IResult ForbidTenant() => Results.Forbid();
static IResult ValidationProblem(string message)
=> Results.ValidationProblem(new Dictionary<string, string[]>
{