save progress
This commit is contained in:
@@ -12,7 +12,7 @@ public sealed class VerdictManifestBuilderTests
|
||||
public void Build_CreatesValidManifest()
|
||||
{
|
||||
var clock = new FakeTimeProvider(DateTimeOffset.Parse("2025-01-01T12:00:00Z"));
|
||||
var builder = new VerdictManifestBuilder(() => "test-manifest-id", clock)
|
||||
var builder = new VerdictManifestBuilder(() => "test-manifest-id")
|
||||
.WithTenant("tenant-1")
|
||||
.WithAsset("sha256:abc123", "CVE-2024-1234")
|
||||
.WithInputs(
|
||||
@@ -61,7 +61,7 @@ public sealed class VerdictManifestBuilderTests
|
||||
|
||||
VerdictManifest BuildManifest(int seed)
|
||||
{
|
||||
return new VerdictManifestBuilder(() => "fixed-id", TimeProvider.System)
|
||||
return new VerdictManifestBuilder(() => "fixed-id")
|
||||
.WithTenant("tenant")
|
||||
.WithAsset("sha256:asset", "CVE-2024-0001")
|
||||
.WithInputs(
|
||||
@@ -106,7 +106,7 @@ public sealed class VerdictManifestBuilderTests
|
||||
{
|
||||
var clock = DateTimeOffset.Parse("2025-01-01T00:00:00Z");
|
||||
|
||||
var manifestA = new VerdictManifestBuilder(() => "id", TimeProvider.System)
|
||||
var manifestA = new VerdictManifestBuilder(() => "id")
|
||||
.WithTenant("t")
|
||||
.WithAsset("sha256:a", "CVE-1")
|
||||
.WithInputs(
|
||||
@@ -119,7 +119,7 @@ public sealed class VerdictManifestBuilderTests
|
||||
.WithClock(clock)
|
||||
.Build();
|
||||
|
||||
var manifestB = new VerdictManifestBuilder(() => "id", TimeProvider.System)
|
||||
var manifestB = new VerdictManifestBuilder(() => "id")
|
||||
.WithTenant("t")
|
||||
.WithAsset("sha256:a", "CVE-1")
|
||||
.WithInputs(
|
||||
@@ -151,7 +151,7 @@ public sealed class VerdictManifestBuilderTests
|
||||
public void Build_NormalizesVulnerabilityIdToUpperCase()
|
||||
{
|
||||
var clock = DateTimeOffset.Parse("2025-01-01T00:00:00Z");
|
||||
var manifest = new VerdictManifestBuilder(() => "id", TimeProvider.System)
|
||||
var manifest = new VerdictManifestBuilder(() => "id")
|
||||
.WithTenant("t")
|
||||
.WithAsset("sha256:a", "cve-2024-1234")
|
||||
.WithInputs(
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Authority.Persistence.Documents;
|
||||
using StellaOps.Authority.Persistence.InMemory.Stores;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Authority.Persistence.Tests;
|
||||
|
||||
public sealed class InMemoryStoreTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task BootstrapInviteStore_AssignsIdAndTimestamps()
|
||||
{
|
||||
var clock = new FakeTimeProvider(DateTimeOffset.Parse("2025-01-02T12:00:00Z"));
|
||||
var idGenerator = new TestIdGenerator("invite-001");
|
||||
var store = new InMemoryBootstrapInviteStore(clock, idGenerator);
|
||||
|
||||
var document = new AuthorityBootstrapInviteDocument
|
||||
{
|
||||
Token = "token-1",
|
||||
Type = "bootstrap",
|
||||
ExpiresAt = clock.GetUtcNow().AddHours(1),
|
||||
};
|
||||
|
||||
var created = await store.CreateAsync(document, CancellationToken.None);
|
||||
|
||||
created.Id.Should().Be("invite-001");
|
||||
created.CreatedAt.Should().Be(clock.GetUtcNow());
|
||||
created.IssuedAt.Should().Be(clock.GetUtcNow());
|
||||
created.Status.Should().Be(AuthorityBootstrapInviteStatuses.Pending);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ServiceAccountStore_UpsertUsesClockAndIdGenerator()
|
||||
{
|
||||
var clock = new FakeTimeProvider(DateTimeOffset.Parse("2025-02-05T08:30:00Z"));
|
||||
var idGenerator = new TestIdGenerator("svc-001");
|
||||
var store = new InMemoryServiceAccountStore(clock, idGenerator);
|
||||
|
||||
var document = new AuthorityServiceAccountDocument
|
||||
{
|
||||
AccountId = "svc-1",
|
||||
Tenant = "tenant-1",
|
||||
DisplayName = "Service",
|
||||
};
|
||||
|
||||
await store.UpsertAsync(document, CancellationToken.None);
|
||||
var fetched = await store.FindByAccountIdAsync("svc-1", CancellationToken.None);
|
||||
|
||||
fetched.Should().NotBeNull();
|
||||
fetched!.Id.Should().Be("svc-001");
|
||||
fetched.CreatedAt.Should().Be(clock.GetUtcNow());
|
||||
fetched.UpdatedAt.Should().Be(clock.GetUtcNow());
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RefreshTokenStore_ConsumeUsesClock()
|
||||
{
|
||||
var clock = new FakeTimeProvider(DateTimeOffset.Parse("2025-03-01T09:15:00Z"));
|
||||
var idGenerator = new TestIdGenerator("refresh-001");
|
||||
var store = new InMemoryRefreshTokenStore(clock, idGenerator);
|
||||
|
||||
var document = new AuthorityRefreshTokenDocument
|
||||
{
|
||||
TokenId = "token-1",
|
||||
SubjectId = "subject-1",
|
||||
};
|
||||
|
||||
await store.UpsertAsync(document, CancellationToken.None);
|
||||
var consumed = await store.ConsumeAsync(document.TokenId, CancellationToken.None);
|
||||
|
||||
consumed.Should().BeTrue();
|
||||
var fetched = await store.FindByTokenIdAsync(document.TokenId, CancellationToken.None);
|
||||
fetched!.ConsumedAt.Should().Be(clock.GetUtcNow());
|
||||
}
|
||||
|
||||
private sealed class TestIdGenerator : IAuthorityInMemoryIdGenerator
|
||||
{
|
||||
private readonly Queue<string> _ids;
|
||||
|
||||
public TestIdGenerator(params string[] ids)
|
||||
{
|
||||
_ids = new Queue<string>(ids);
|
||||
}
|
||||
|
||||
public string NextId()
|
||||
{
|
||||
if (_ids.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("No more IDs available.");
|
||||
}
|
||||
|
||||
return _ids.Dequeue();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using StellaOps.Authority.Persistence.Postgres.Models;
|
||||
using StellaOps.Authority.Persistence.Postgres.Repositories;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Authority.Persistence.Tests.TestDoubles;
|
||||
|
||||
@@ -146,6 +147,9 @@ internal sealed class InMemoryUserRepository : IUserRepository
|
||||
public Task<UserEntity?> GetByUsernameAsync(string tenantId, string username, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(_users.Values.FirstOrDefault(u => u.TenantId == tenantId && u.Username == username));
|
||||
|
||||
public Task<UserEntity?> GetBySubjectIdAsync(string tenantId, string subjectId, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(_users.Values.FirstOrDefault(u => u.TenantId == tenantId && MatchesSubject(u.Metadata, subjectId)));
|
||||
|
||||
public Task<UserEntity?> GetByEmailAsync(string tenantId, string email, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(_users.Values.FirstOrDefault(u => u.TenantId == tenantId && u.Email == email));
|
||||
|
||||
@@ -198,6 +202,34 @@ internal sealed class InMemoryUserRepository : IUserRepository
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<UserEntity> Snapshot() => _users.Values.ToList();
|
||||
|
||||
private static bool MatchesSubject(string? metadataJson, string subjectId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(metadataJson) || string.IsNullOrWhiteSpace(subjectId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var document = JsonDocument.Parse(metadataJson);
|
||||
if (document.RootElement.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (document.RootElement.TryGetProperty("subjectId", out var subjectElement)
|
||||
&& subjectElement.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
return string.Equals(subjectElement.GetString(), subjectId, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
internal static class AuthorityCloneHelpers
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Threading;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Npgsql;
|
||||
using StellaOps.Authority.Core.Verdicts;
|
||||
using StellaOps.Authority.Persistence.Postgres;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Authority.Persistence.Tests;
|
||||
|
||||
[Collection(AuthorityPostgresCollection.Name)]
|
||||
public sealed class VerdictManifestStoreTests : IAsyncLifetime
|
||||
{
|
||||
private readonly AuthorityPostgresFixture _fixture;
|
||||
private readonly AuthorityDataSource _dataSource;
|
||||
private readonly PostgresVerdictManifestStore _store;
|
||||
|
||||
public VerdictManifestStoreTests(AuthorityPostgresFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
var options = fixture.CreateOptions();
|
||||
_dataSource = new AuthorityDataSource(Options.Create(options), NullLogger<AuthorityDataSource>.Instance);
|
||||
_store = new PostgresVerdictManifestStore(_dataSource);
|
||||
}
|
||||
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
await _fixture.TruncateAllTablesAsync();
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync() => _dataSource.DisposeAsync();
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task StoreAndGetById_RoundTripsManifest()
|
||||
{
|
||||
var evaluatedAt = DateTimeOffset.Parse("2025-01-15T10:00:00Z");
|
||||
var manifest = CreateManifest("tenant-1", "manifest-001", evaluatedAt, VexStatus.NotAffected);
|
||||
|
||||
await _store.StoreAsync(manifest);
|
||||
var fetched = await _store.GetByIdAsync(manifest.Tenant, manifest.ManifestId);
|
||||
|
||||
fetched.Should().NotBeNull();
|
||||
fetched!.ManifestId.Should().Be(manifest.ManifestId);
|
||||
fetched.AssetDigest.Should().Be(manifest.AssetDigest);
|
||||
fetched.VulnerabilityId.Should().Be(manifest.VulnerabilityId);
|
||||
fetched.PolicyHash.Should().Be(manifest.PolicyHash);
|
||||
fetched.LatticeVersion.Should().Be(manifest.LatticeVersion);
|
||||
fetched.EvaluatedAt.Should().Be(evaluatedAt);
|
||||
fetched.Result.Status.Should().Be(VexStatus.NotAffected);
|
||||
fetched.Inputs.SbomDigests.Should().Contain("sbom:sha256:aaa");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task StoreAsync_WritesStringEnumJson()
|
||||
{
|
||||
var evaluatedAt = DateTimeOffset.Parse("2025-01-15T11:00:00Z");
|
||||
var manifest = CreateManifest("tenant-2", "manifest-002", evaluatedAt, VexStatus.UnderInvestigation);
|
||||
|
||||
await _store.StoreAsync(manifest);
|
||||
|
||||
await using var conn = await _dataSource.OpenConnectionAsync(manifest.Tenant, "reader", CancellationToken.None);
|
||||
await using var cmd = new NpgsqlCommand("""
|
||||
SELECT result_json::text
|
||||
FROM verdict_manifests
|
||||
WHERE tenant = @tenant AND manifest_id = @manifestId
|
||||
""", conn)
|
||||
{
|
||||
CommandTimeout = _dataSource.CommandTimeoutSeconds,
|
||||
};
|
||||
cmd.Parameters.AddWithValue("tenant", manifest.Tenant);
|
||||
cmd.Parameters.AddWithValue("manifestId", manifest.ManifestId);
|
||||
|
||||
var json = (string?)await cmd.ExecuteScalarAsync();
|
||||
json.Should().NotBeNull();
|
||||
json.Should().Contain("\"status\":\"under_investigation\"");
|
||||
}
|
||||
|
||||
private static VerdictManifest CreateManifest(string tenant, string manifestId, DateTimeOffset evaluatedAt, VexStatus status)
|
||||
{
|
||||
var inputs = new VerdictInputs
|
||||
{
|
||||
SbomDigests = ImmutableArray.Create("sbom:sha256:aaa"),
|
||||
VulnFeedSnapshotIds = ImmutableArray.Create("feed:1"),
|
||||
VexDocumentDigests = ImmutableArray.Create("vex:1"),
|
||||
ReachabilityGraphIds = ImmutableArray.Create("graph:1"),
|
||||
ClockCutoff = evaluatedAt.AddMinutes(-5),
|
||||
};
|
||||
|
||||
var result = new VerdictResult
|
||||
{
|
||||
Status = status,
|
||||
Confidence = 0.82,
|
||||
Explanations = ImmutableArray.Create(new VerdictExplanation
|
||||
{
|
||||
SourceId = "source-1",
|
||||
Reason = "policy-pass",
|
||||
ProvenanceScore = 0.9,
|
||||
CoverageScore = 0.8,
|
||||
ReplayabilityScore = 0.95,
|
||||
StrengthMultiplier = 1.0,
|
||||
FreshnessMultiplier = 0.97,
|
||||
ClaimScore = 0.88,
|
||||
AssertedStatus = status,
|
||||
Accepted = true,
|
||||
}),
|
||||
EvidenceRefs = ImmutableArray.Create("evidence-1"),
|
||||
HasConflicts = false,
|
||||
RequiresReplayProof = false,
|
||||
};
|
||||
|
||||
var manifest = new VerdictManifest
|
||||
{
|
||||
ManifestId = manifestId,
|
||||
Tenant = tenant,
|
||||
AssetDigest = "sha256:asset-1",
|
||||
VulnerabilityId = "CVE-2025-0001",
|
||||
Inputs = inputs,
|
||||
Result = result,
|
||||
PolicyHash = "policy-hash-1",
|
||||
LatticeVersion = "lattice-1",
|
||||
EvaluatedAt = evaluatedAt,
|
||||
ManifestDigest = string.Empty,
|
||||
SignatureBase64 = null,
|
||||
RekorLogId = null,
|
||||
};
|
||||
|
||||
var digest = VerdictManifestSerializer.ComputeDigest(manifest);
|
||||
return manifest with { ManifestDigest = digest };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user