Close admin trust audit gaps and stabilize live sweeps

This commit is contained in:
master
2026-03-12 10:14:00 +02:00
parent a00efb7ab2
commit 6964a046a5
50 changed files with 5968 additions and 2850 deletions

View File

@@ -223,11 +223,29 @@ public static class PackAdapterEndpoints
.WithSummary("Pack v2 administration A5 policy governance projection.")
.RequireAuthorization(PlatformPolicies.SetupRead);
administration.MapGet("/trust-signing", (
administration.MapGet("/trust-signing", async (
HttpContext context,
PlatformRequestContextResolver resolver) =>
PlatformRequestContextResolver resolver,
IAdministrationTrustSigningStore trustSigningStore,
CancellationToken cancellationToken) =>
{
return BuildAdministrationItem(context, resolver, BuildAdministrationTrustSigning);
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var payload = await BuildAdministrationTrustSigningAsync(
requestContext!.TenantId,
trustSigningStore,
cancellationToken).ConfigureAwait(false);
return Results.Ok(new PlatformItemResponse<AdministrationTrustSigningDto>(
requestContext.TenantId,
requestContext.ActorId,
SnapshotAt,
Cached: false,
CacheTtlSeconds: 0,
payload));
})
.WithName("GetAdministrationTrustSigning")
.WithSummary("Pack v2 administration A6 trust and signing projection.")
@@ -540,14 +558,41 @@ public static class PackAdapterEndpoints
]);
}
private static AdministrationTrustSigningDto BuildAdministrationTrustSigning()
private static async Task<AdministrationTrustSigningDto> BuildAdministrationTrustSigningAsync(
string tenantId,
IAdministrationTrustSigningStore trustSigningStore,
CancellationToken cancellationToken)
{
var keys = await trustSigningStore.ListKeysAsync(tenantId, 500, 0, cancellationToken).ConfigureAwait(false);
var issuers = await trustSigningStore.ListIssuersAsync(tenantId, 500, 0, cancellationToken).ConfigureAwait(false);
var certificates = await trustSigningStore.ListCertificatesAsync(tenantId, 500, 0, cancellationToken).ConfigureAwait(false);
var transparencyConfig = await trustSigningStore.GetTransparencyLogConfigAsync(tenantId, cancellationToken).ConfigureAwait(false);
var expiringCertificateCount = certificates.Count(certificate =>
!string.Equals(certificate.Status, "revoked", StringComparison.OrdinalIgnoreCase)
&& certificate.NotAfter <= SnapshotAt.AddDays(10));
var signals = new[]
{
new AdministrationTrustSignalDto("audit-log", "healthy", "Audit log ingestion is current."),
new AdministrationTrustSignalDto("certificate-expiry", "warning", "1 certificate expires within 10 days."),
new AdministrationTrustSignalDto("transparency-log", "healthy", "Rekor witness is reachable."),
new AdministrationTrustSignalDto("trust-scoring", "healthy", "Issuer trust score recalculation completed."),
new AdministrationTrustSignalDto("audit-log", "healthy", "Trust-signing configuration changes are being recorded."),
new AdministrationTrustSignalDto(
"certificate-expiry",
expiringCertificateCount > 0 ? "warning" : "healthy",
expiringCertificateCount > 0
? $"{expiringCertificateCount} certificate{(expiringCertificateCount == 1 ? string.Empty : "s")} expire within 10 days."
: "No certificate expirations are due in the next 10 days."),
new AdministrationTrustSignalDto(
"transparency-log",
transparencyConfig is null ? "warning" : "healthy",
transparencyConfig is null
? "Transparency log is not configured for this tenant."
: $"Transparency log witness points to {transparencyConfig.LogUrl}."),
new AdministrationTrustSignalDto(
"trust-scoring",
issuers.Count == 0 ? "warning" : "healthy",
issuers.Count == 0
? "No trusted issuers are registered yet."
: "Issuer trust inventory is available for scoring and review."),
}.OrderBy(signal => signal.SignalId, StringComparer.Ordinal).ToList();
var aliases = BuildAdministrationAliases(
@@ -558,7 +603,7 @@ public static class PackAdapterEndpoints
]);
return new AdministrationTrustSigningDto(
Inventory: new AdministrationTrustInventoryDto(Keys: 14, Issuers: 7, Certificates: 23),
Inventory: new AdministrationTrustInventoryDto(Keys: keys.Count, Issuers: issuers.Count, Certificates: certificates.Count),
Signals: signals,
LegacyAliases: aliases,
EvidenceConsumerPath: "/evidence-audit/proofs");

View File

@@ -32,7 +32,7 @@ public sealed class PostgresAdministrationTrustSigningStore : IAdministrationTru
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var tenantGuid = ParseTenantId(tenantId);
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
var normalizedLimit = NormalizeLimit(limit);
var normalizedOffset = NormalizeOffset(offset);
@@ -83,7 +83,7 @@ public sealed class PostgresAdministrationTrustSigningStore : IAdministrationTru
var actor = NormalizeActor(actorId);
var now = _timeProvider.GetUtcNow();
var keyId = Guid.NewGuid();
var tenantGuid = ParseTenantId(tenantId);
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
await using var connection = await OpenTenantConnectionAsync(tenantGuid, cancellationToken).ConfigureAwait(false);
try
@@ -164,7 +164,7 @@ public sealed class PostgresAdministrationTrustSigningStore : IAdministrationTru
var actor = NormalizeActor(actorId);
var now = _timeProvider.GetUtcNow();
var tenantGuid = ParseTenantId(tenantId);
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
await using var connection = await OpenTenantConnectionAsync(tenantGuid, cancellationToken).ConfigureAwait(false);
var existingStatus = await GetKeyStatusAsync(connection, tenantGuid, keyId, cancellationToken).ConfigureAwait(false);
@@ -226,7 +226,7 @@ public sealed class PostgresAdministrationTrustSigningStore : IAdministrationTru
_ = NormalizeRequired(request.Reason, "reason_required");
var actor = NormalizeActor(actorId);
var now = _timeProvider.GetUtcNow();
var tenantGuid = ParseTenantId(tenantId);
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
await using var connection = await OpenTenantConnectionAsync(tenantGuid, cancellationToken).ConfigureAwait(false);
await using var command = new NpgsqlCommand(
@@ -270,7 +270,7 @@ public sealed class PostgresAdministrationTrustSigningStore : IAdministrationTru
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var tenantGuid = ParseTenantId(tenantId);
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
var normalizedLimit = NormalizeLimit(limit);
var normalizedOffset = NormalizeOffset(offset);
@@ -323,7 +323,7 @@ public sealed class PostgresAdministrationTrustSigningStore : IAdministrationTru
var actor = NormalizeActor(actorId);
var now = _timeProvider.GetUtcNow();
var issuerId = Guid.NewGuid();
var tenantGuid = ParseTenantId(tenantId);
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
await using var connection = await OpenTenantConnectionAsync(tenantGuid, cancellationToken).ConfigureAwait(false);
try
@@ -397,7 +397,7 @@ public sealed class PostgresAdministrationTrustSigningStore : IAdministrationTru
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var tenantGuid = ParseTenantId(tenantId);
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
var normalizedLimit = NormalizeLimit(limit);
var normalizedOffset = NormalizeOffset(offset);
@@ -454,7 +454,7 @@ public sealed class PostgresAdministrationTrustSigningStore : IAdministrationTru
var actor = NormalizeActor(actorId);
var now = _timeProvider.GetUtcNow();
var certificateId = Guid.NewGuid();
var tenantGuid = ParseTenantId(tenantId);
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
await using var connection = await OpenTenantConnectionAsync(tenantGuid, cancellationToken).ConfigureAwait(false);
@@ -571,7 +571,7 @@ public sealed class PostgresAdministrationTrustSigningStore : IAdministrationTru
_ = NormalizeRequired(request.Reason, "reason_required");
var actor = NormalizeActor(actorId);
var now = _timeProvider.GetUtcNow();
var tenantGuid = ParseTenantId(tenantId);
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
await using var connection = await OpenTenantConnectionAsync(tenantGuid, cancellationToken).ConfigureAwait(false);
await using var command = new NpgsqlCommand(
@@ -615,7 +615,7 @@ public sealed class PostgresAdministrationTrustSigningStore : IAdministrationTru
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var tenantGuid = ParseTenantId(tenantId);
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
await using var connection = await OpenTenantConnectionAsync(tenantGuid, cancellationToken).ConfigureAwait(false);
await using var command = new NpgsqlCommand(
@@ -660,7 +660,7 @@ public sealed class PostgresAdministrationTrustSigningStore : IAdministrationTru
var actor = NormalizeActor(actorId);
var now = _timeProvider.GetUtcNow();
var tenantGuid = ParseTenantId(tenantId);
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
await using var connection = await OpenTenantConnectionAsync(tenantGuid, cancellationToken).ConfigureAwait(false);
await using var command = new NpgsqlCommand(
@@ -762,21 +762,6 @@ public sealed class PostgresAdministrationTrustSigningStore : IAdministrationTru
return connection;
}
private static Guid ParseTenantId(string tenantId)
{
if (string.IsNullOrWhiteSpace(tenantId))
{
throw new InvalidOperationException("tenant_required");
}
if (!Guid.TryParse(tenantId, out var tenantGuid))
{
throw new InvalidOperationException("tenant_id_invalid");
}
return tenantGuid;
}
private static int NormalizeLimit(int limit) => limit < 1 ? 1 : limit;
private static int NormalizeOffset(int offset) => offset < 0 ? 0 : offset;

View File

@@ -70,7 +70,7 @@ public sealed class PostgresReleaseControlBundleStore : IReleaseControlBundleSto
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var tenantGuid = ParseTenantId(tenantId);
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
await using var connection = await OpenTenantConnectionAsync(tenantGuid, cancellationToken).ConfigureAwait(false);
await using var command = new NpgsqlCommand(
@@ -137,7 +137,7 @@ public sealed class PostgresReleaseControlBundleStore : IReleaseControlBundleSto
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var tenantGuid = ParseTenantId(tenantId);
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
await using var connection = await OpenTenantConnectionAsync(tenantGuid, cancellationToken).ConfigureAwait(false);
await using var command = new NpgsqlCommand(
@@ -220,7 +220,7 @@ public sealed class PostgresReleaseControlBundleStore : IReleaseControlBundleSto
throw new InvalidOperationException("bundle_name_required");
}
var tenantGuid = ParseTenantId(tenantId);
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
var now = _timeProvider.GetUtcNow();
var bundleId = Guid.NewGuid();
var createdBy = NormalizeActor(actorId);
@@ -273,7 +273,7 @@ public sealed class PostgresReleaseControlBundleStore : IReleaseControlBundleSto
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var tenantGuid = ParseTenantId(tenantId);
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
await using var connection = await OpenTenantConnectionAsync(tenantGuid, cancellationToken).ConfigureAwait(false);
@@ -325,7 +325,7 @@ public sealed class PostgresReleaseControlBundleStore : IReleaseControlBundleSto
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var tenantGuid = ParseTenantId(tenantId);
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
await using var connection = await OpenTenantConnectionAsync(tenantGuid, cancellationToken).ConfigureAwait(false);
@@ -395,7 +395,7 @@ public sealed class PostgresReleaseControlBundleStore : IReleaseControlBundleSto
throw new InvalidOperationException("request_required");
}
var tenantGuid = ParseTenantId(tenantId);
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
var createdBy = NormalizeActor(actorId);
var now = _timeProvider.GetUtcNow();
var normalizedComponents = ReleaseControlBundleDigest.NormalizeComponents(request.Components);
@@ -544,7 +544,7 @@ public sealed class PostgresReleaseControlBundleStore : IReleaseControlBundleSto
throw new InvalidOperationException("request_required");
}
var tenantGuid = ParseTenantId(tenantId);
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
var requestedBy = NormalizeActor(actorId);
var now = _timeProvider.GetUtcNow();
var normalizedIdempotencyKey = NormalizeIdempotencyKey(request.IdempotencyKey);
@@ -656,7 +656,7 @@ public sealed class PostgresReleaseControlBundleStore : IReleaseControlBundleSto
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var tenantGuid = ParseTenantId(tenantId);
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
await using var connection = await OpenTenantConnectionAsync(tenantGuid, cancellationToken).ConfigureAwait(false);
await using var command = new NpgsqlCommand(ListMaterializationRunsSql, connection);
@@ -682,7 +682,7 @@ public sealed class PostgresReleaseControlBundleStore : IReleaseControlBundleSto
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var tenantGuid = ParseTenantId(tenantId);
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
await using var connection = await OpenTenantConnectionAsync(tenantGuid, cancellationToken).ConfigureAwait(false);
await using var command = new NpgsqlCommand(ListMaterializationRunsByBundleSql, connection);
@@ -707,7 +707,7 @@ public sealed class PostgresReleaseControlBundleStore : IReleaseControlBundleSto
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var tenantGuid = ParseTenantId(tenantId);
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
await using var connection = await OpenTenantConnectionAsync(tenantGuid, cancellationToken).ConfigureAwait(false);
await using var command = new NpgsqlCommand(
@@ -772,24 +772,6 @@ public sealed class PostgresReleaseControlBundleStore : IReleaseControlBundleSto
UpdatedAt: reader.GetFieldValue<DateTimeOffset>(9));
}
private static Guid ParseTenantId(string tenantId)
{
if (string.IsNullOrWhiteSpace(tenantId))
{
throw new InvalidOperationException("tenant_required");
}
if (Guid.TryParse(tenantId, out var tenantGuid))
{
return tenantGuid;
}
// Derive deterministic GUID from string tenant identifier (e.g. "default", "demo-prod")
var hash = System.Security.Cryptography.SHA256.HashData(
System.Text.Encoding.UTF8.GetBytes(tenantId));
return new Guid(hash.AsSpan(0, 16));
}
private async Task<NpgsqlConnection> OpenTenantConnectionAsync(Guid tenantId, CancellationToken cancellationToken)
{
var connection = await _dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);

View File

@@ -0,0 +1,24 @@
using System.Security.Cryptography;
using System.Text;
namespace StellaOps.Platform.WebService.Services;
internal static class TenantStorageKey
{
public static Guid ParseTenantGuid(string tenantId)
{
if (string.IsNullOrWhiteSpace(tenantId))
{
throw new InvalidOperationException("tenant_required");
}
var normalizedTenantId = tenantId.Trim();
if (Guid.TryParse(normalizedTenantId, out var tenantGuid))
{
return tenantGuid;
}
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(normalizedTenantId));
return new Guid(hash.AsSpan(0, 16));
}
}

View File

@@ -43,6 +43,10 @@
<EmbeddedResource Include="Translations\*.json" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="StellaOps.Platform.WebService.Tests" />
</ItemGroup>
<PropertyGroup Label="StellaOpsReleaseVersion">
<Version>1.0.0-alpha1</Version>
<InformationalVersion>1.0.0-alpha1</InformationalVersion>

View File

@@ -0,0 +1,242 @@
-- Scratch-install trust-signing seed for canonical demo tenants.
-- Tenant GUIDs are derived with the same deterministic SHA-256 -> Guid mapping used in Platform stores.
INSERT INTO release.trust_keys (
id,
tenant_id,
key_alias,
algorithm,
status,
current_version,
metadata_json,
created_at,
updated_at,
created_by,
updated_by
)
SELECT
'a9adf36f-2f4d-4f31-9b0b-138e3f6f1f61'::uuid,
'3a5e72b6-ae6a-f8a4-2b6a-df2960d63016'::uuid,
'demo-prod-core-signing',
'ed25519',
'active',
3,
'{"owner":"secops","region":"us-east"}'::jsonb,
'2026-03-01T00:00:00Z'::timestamptz,
'2026-03-09T00:00:00Z'::timestamptz,
'system',
'system'
WHERE NOT EXISTS (
SELECT 1
FROM release.trust_keys
WHERE tenant_id = '3a5e72b6-ae6a-f8a4-2b6a-df2960d63016'::uuid
AND lower(key_alias) = lower('demo-prod-core-signing')
);
INSERT INTO release.trust_issuers (
id,
tenant_id,
issuer_name,
issuer_uri,
trust_level,
status,
created_at,
updated_at,
created_by,
updated_by
)
SELECT
'4ac7e1d4-7a2e-4b4d-9e12-5d42e3168a91'::uuid,
'3a5e72b6-ae6a-f8a4-2b6a-df2960d63016'::uuid,
'Demo Prod Root CA',
'https://issuer.demo-prod.stella-ops.local/root',
'high',
'active',
'2026-03-01T00:00:00Z'::timestamptz,
'2026-03-09T00:00:00Z'::timestamptz,
'system',
'system'
WHERE NOT EXISTS (
SELECT 1
FROM release.trust_issuers
WHERE tenant_id = '3a5e72b6-ae6a-f8a4-2b6a-df2960d63016'::uuid
AND lower(issuer_uri) = lower('https://issuer.demo-prod.stella-ops.local/root')
);
INSERT INTO release.trust_certificates (
id,
tenant_id,
key_id,
issuer_id,
serial_number,
status,
not_before,
not_after,
created_at,
updated_at,
created_by,
updated_by
)
SELECT
'8d1f0b75-3d56-4d8f-a40b-52f5e52e7fb1'::uuid,
'3a5e72b6-ae6a-f8a4-2b6a-df2960d63016'::uuid,
'a9adf36f-2f4d-4f31-9b0b-138e3f6f1f61'::uuid,
'4ac7e1d4-7a2e-4b4d-9e12-5d42e3168a91'::uuid,
'DEMO-PROD-SER-0001',
'active',
'2026-03-01T00:00:00Z'::timestamptz,
'2026-03-18T00:00:00Z'::timestamptz,
'2026-03-01T00:00:00Z'::timestamptz,
'2026-03-09T00:00:00Z'::timestamptz,
'system',
'system'
WHERE NOT EXISTS (
SELECT 1
FROM release.trust_certificates
WHERE tenant_id = '3a5e72b6-ae6a-f8a4-2b6a-df2960d63016'::uuid
AND lower(serial_number) = lower('DEMO-PROD-SER-0001')
);
INSERT INTO release.trust_transparency_configs (
tenant_id,
log_url,
witness_url,
enforce_inclusion,
updated_at,
updated_by
)
VALUES (
'3a5e72b6-ae6a-f8a4-2b6a-df2960d63016'::uuid,
'https://rekor.demo-prod.stella-ops.local',
'https://rekor-witness.demo-prod.stella-ops.local',
true,
'2026-03-09T00:00:00Z'::timestamptz,
'system'
)
ON CONFLICT (tenant_id) DO UPDATE
SET
log_url = EXCLUDED.log_url,
witness_url = EXCLUDED.witness_url,
enforce_inclusion = EXCLUDED.enforce_inclusion,
updated_at = EXCLUDED.updated_at,
updated_by = EXCLUDED.updated_by;
INSERT INTO release.trust_keys (
id,
tenant_id,
key_alias,
algorithm,
status,
current_version,
metadata_json,
created_at,
updated_at,
created_by,
updated_by
)
SELECT
'5fdf7b2d-9d32-4b69-874f-9b22a4d22f21'::uuid,
'c1eea837-19ce-7d68-132f-e29051dca629'::uuid,
'default-core-signing',
'ed25519',
'active',
1,
'{"owner":"bootstrap","region":"global"}'::jsonb,
'2026-03-01T00:00:00Z'::timestamptz,
'2026-03-01T00:00:00Z'::timestamptz,
'system',
'system'
WHERE NOT EXISTS (
SELECT 1
FROM release.trust_keys
WHERE tenant_id = 'c1eea837-19ce-7d68-132f-e29051dca629'::uuid
AND lower(key_alias) = lower('default-core-signing')
);
INSERT INTO release.trust_issuers (
id,
tenant_id,
issuer_name,
issuer_uri,
trust_level,
status,
created_at,
updated_at,
created_by,
updated_by
)
SELECT
'f7d0505e-4a94-4688-a046-87f7a9c7cf76'::uuid,
'c1eea837-19ce-7d68-132f-e29051dca629'::uuid,
'Default Root CA',
'https://issuer.default.stella-ops.local/root',
'high',
'active',
'2026-03-01T00:00:00Z'::timestamptz,
'2026-03-01T00:00:00Z'::timestamptz,
'system',
'system'
WHERE NOT EXISTS (
SELECT 1
FROM release.trust_issuers
WHERE tenant_id = 'c1eea837-19ce-7d68-132f-e29051dca629'::uuid
AND lower(issuer_uri) = lower('https://issuer.default.stella-ops.local/root')
);
INSERT INTO release.trust_certificates (
id,
tenant_id,
key_id,
issuer_id,
serial_number,
status,
not_before,
not_after,
created_at,
updated_at,
created_by,
updated_by
)
SELECT
'c0d9c7db-a0c8-41e9-a7f4-e8f03e4b31e3'::uuid,
'c1eea837-19ce-7d68-132f-e29051dca629'::uuid,
'5fdf7b2d-9d32-4b69-874f-9b22a4d22f21'::uuid,
'f7d0505e-4a94-4688-a046-87f7a9c7cf76'::uuid,
'DEFAULT-SER-0001',
'active',
'2026-03-01T00:00:00Z'::timestamptz,
'2026-09-01T00:00:00Z'::timestamptz,
'2026-03-01T00:00:00Z'::timestamptz,
'2026-03-01T00:00:00Z'::timestamptz,
'system',
'system'
WHERE NOT EXISTS (
SELECT 1
FROM release.trust_certificates
WHERE tenant_id = 'c1eea837-19ce-7d68-132f-e29051dca629'::uuid
AND lower(serial_number) = lower('DEFAULT-SER-0001')
);
INSERT INTO release.trust_transparency_configs (
tenant_id,
log_url,
witness_url,
enforce_inclusion,
updated_at,
updated_by
)
VALUES (
'c1eea837-19ce-7d68-132f-e29051dca629'::uuid,
'https://rekor.default.stella-ops.local',
'https://rekor-witness.default.stella-ops.local',
true,
'2026-03-01T00:00:00Z'::timestamptz,
'system'
)
ON CONFLICT (tenant_id) DO UPDATE
SET
log_url = EXCLUDED.log_url,
witness_url = EXCLUDED.witness_url,
enforce_inclusion = EXCLUDED.enforce_inclusion,
updated_at = EXCLUDED.updated_at,
updated_by = EXCLUDED.updated_by;

View File

@@ -1,8 +1,10 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Platform.WebService.Contracts;
using System.Linq;
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using StellaOps.Platform.WebService.Constants;
using StellaOps.TestKit;
@@ -146,6 +148,75 @@ public sealed class PackAdapterEndpointsTests : IClassFixture<PlatformWebApplica
Assert.DoesNotContain(PlatformPolicies.SetupRead, policies);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task TrustSigningOverview_Uses_live_inventory_counts_for_selected_tenant()
{
using var client = CreateTenantClient("demo-prod");
var keyResponse = await client.PostAsJsonAsync(
"/api/v1/administration/trust-signing/keys",
new CreateAdministrationTrustKeyRequest(
Alias: "tenant-live-key",
Algorithm: "ed25519",
MetadataJson: "{\"owner\":\"secops\"}"),
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Created, keyResponse.StatusCode);
var key = await keyResponse.Content.ReadFromJsonAsync<AdministrationTrustKeySummary>(TestContext.Current.CancellationToken);
Assert.NotNull(key);
var issuerResponse = await client.PostAsJsonAsync(
"/api/v1/administration/trust-signing/issuers",
new RegisterAdministrationTrustIssuerRequest(
Name: "Tenant Live Root CA",
IssuerUri: "https://issuer.demo-prod.stella-ops.local/live",
TrustLevel: "high"),
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Created, issuerResponse.StatusCode);
var issuer = await issuerResponse.Content.ReadFromJsonAsync<AdministrationTrustIssuerSummary>(TestContext.Current.CancellationToken);
Assert.NotNull(issuer);
var certificateResponse = await client.PostAsJsonAsync(
"/api/v1/administration/trust-signing/certificates",
new RegisterAdministrationTrustCertificateRequest(
KeyId: key!.KeyId,
IssuerId: issuer!.IssuerId,
SerialNumber: "TENANT-LIVE-SER-0001",
NotBefore: DateTimeOffset.Parse("2026-02-01T00:00:00Z"),
NotAfter: DateTimeOffset.Parse("2026-02-25T00:00:00Z")),
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Created, certificateResponse.StatusCode);
var configureResponse = await client.PutAsJsonAsync(
"/api/v1/administration/trust-signing/transparency-log",
new ConfigureAdministrationTransparencyLogRequest(
LogUrl: "https://rekor.demo-prod.stella-ops.local",
WitnessUrl: "https://rekor-witness.demo-prod.stella-ops.local",
EnforceInclusion: true),
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, configureResponse.StatusCode);
var overviewResponse = await client.GetAsync("/api/v1/administration/trust-signing", TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, overviewResponse.StatusCode);
using var document = JsonDocument.Parse(await overviewResponse.Content.ReadAsStringAsync(TestContext.Current.CancellationToken));
var item = document.RootElement.GetProperty("item");
Assert.Equal(1, item.GetProperty("inventory").GetProperty("keys").GetInt32());
Assert.Equal(1, item.GetProperty("inventory").GetProperty("issuers").GetInt32());
Assert.Equal(1, item.GetProperty("inventory").GetProperty("certificates").GetInt32());
var signals = item
.GetProperty("signals")
.EnumerateArray()
.ToDictionary(
signal => signal.GetProperty("signalId").GetString()!,
signal => signal.GetProperty("status").GetString()!,
StringComparer.Ordinal);
Assert.Equal("warning", signals["certificate-expiry"]);
Assert.Equal("healthy", signals["transparency-log"]);
}
private HttpClient CreateTenantClient(string tenantId)
{
var client = _factory.CreateClient();

View File

@@ -0,0 +1,26 @@
using FluentAssertions;
using StellaOps.Platform.WebService.Services;
using StellaOps.TestKit;
namespace StellaOps.Platform.WebService.Tests;
public sealed class TenantStorageKeyTests
{
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData("demo-prod", "3a5e72b6-ae6a-f8a4-2b6a-df2960d63016")]
[InlineData("default", "c1eea837-19ce-7d68-132f-e29051dca629")]
public void ParseTenantGuid_derives_deterministic_guid_for_slug_tenants(string tenantId, string expectedGuid)
{
TenantStorageKey.ParseTenantGuid(tenantId).Should().Be(Guid.Parse(expectedGuid));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ParseTenantGuid_returns_existing_guid_without_rehashing()
{
var tenantGuid = Guid.NewGuid();
TenantStorageKey.ParseTenantGuid(tenantGuid.ToString("D")).Should().Be(tenantGuid);
}
}