save progress
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
// JsonCanonicalizerTests - RFC 8785 canonicalization tests for TrustVerdict
|
||||
// Part of SPRINT_1227_0004_0004: Signed TrustVerdict Attestations
|
||||
|
||||
using FluentAssertions;
|
||||
using StellaOps.Attestor.TrustVerdict;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Attestor.TrustVerdict.Tests;
|
||||
|
||||
public class JsonCanonicalizerTests
|
||||
{
|
||||
[Fact]
|
||||
public void Canonicalize_OrdersKeysAndRemovesWhitespace()
|
||||
{
|
||||
var input = "{ \"b\": 1, \"a\": 2 }";
|
||||
|
||||
var canonical = JsonCanonicalizer.Canonicalize(input);
|
||||
|
||||
canonical.Should().Be("{\"a\":2,\"b\":1}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Canonicalize_NormalizesExponentNumbers()
|
||||
{
|
||||
var input = "{\"n\":1e0}";
|
||||
|
||||
var canonical = JsonCanonicalizer.Canonicalize(input);
|
||||
|
||||
canonical.Should().Be("{\"n\":1}");
|
||||
}
|
||||
}
|
||||
@@ -63,14 +63,17 @@ public class TrustEvidenceMerkleBuilderTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_SortsItemsByDigest()
|
||||
public void Build_SortsItemsByDigestWithTieBreakers()
|
||||
{
|
||||
// Arrange - Items in reverse order
|
||||
// Arrange - Items with matching digests but different tie-breakers
|
||||
var items = new[]
|
||||
{
|
||||
CreateEvidenceItem("sha256:zzz"),
|
||||
CreateEvidenceItem("sha256:aaa"),
|
||||
CreateEvidenceItem("sha256:mmm")
|
||||
CreateEvidenceItem("sha256:dup", TrustEvidenceTypes.Signature, "https://example.com/b", "b",
|
||||
new DateTimeOffset(2025, 1, 15, 12, 2, 0, TimeSpan.Zero)),
|
||||
CreateEvidenceItem("sha256:dup", TrustEvidenceTypes.Certificate, "https://example.com/a", "a",
|
||||
new DateTimeOffset(2025, 1, 15, 12, 1, 0, TimeSpan.Zero)),
|
||||
CreateEvidenceItem("sha256:dup", TrustEvidenceTypes.VexDocument, "https://example.com/a", "c",
|
||||
new DateTimeOffset(2025, 1, 15, 12, 3, 0, TimeSpan.Zero))
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -78,7 +81,16 @@ public class TrustEvidenceMerkleBuilderTests
|
||||
|
||||
// Assert
|
||||
tree.LeafCount.Should().Be(3);
|
||||
// First leaf should correspond to "sha256:aaa"
|
||||
var expectedOrder = items
|
||||
.OrderBy(i => i.Digest, StringComparer.Ordinal)
|
||||
.ThenBy(i => i.Type, StringComparer.Ordinal)
|
||||
.ThenBy(i => i.Uri ?? string.Empty, StringComparer.Ordinal)
|
||||
.ThenBy(i => i.Description ?? string.Empty, StringComparer.Ordinal)
|
||||
.ThenBy(i => i.CollectedAt?.ToUniversalTime())
|
||||
.Select(i => ToDigestString(_builder.ComputeLeafHash(i)))
|
||||
.ToList();
|
||||
|
||||
tree.LeafHashes.Should().BeEquivalentTo(expectedOrder, opts => opts.WithStrictOrdering());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -174,7 +186,13 @@ public class TrustEvidenceMerkleBuilderTests
|
||||
var proof = tree.GenerateProof(1);
|
||||
|
||||
// Get the item at sorted index 1 (should be "sha256:bbb")
|
||||
var sortedItems = items.OrderBy(i => i.Digest).ToList();
|
||||
var sortedItems = items
|
||||
.OrderBy(i => i.Digest, StringComparer.Ordinal)
|
||||
.ThenBy(i => i.Type, StringComparer.Ordinal)
|
||||
.ThenBy(i => i.Uri ?? string.Empty, StringComparer.Ordinal)
|
||||
.ThenBy(i => i.Description ?? string.Empty, StringComparer.Ordinal)
|
||||
.ThenBy(i => i.CollectedAt?.ToUniversalTime())
|
||||
.ToList();
|
||||
var item = sortedItems[1];
|
||||
|
||||
// Act
|
||||
@@ -259,7 +277,13 @@ public class TrustEvidenceMerkleBuilderTests
|
||||
CreateEvidenceItem("sha256:bbb")
|
||||
};
|
||||
var tree = _builder.Build(items);
|
||||
var chain = tree.ToEvidenceChain(items.OrderBy(i => i.Digest).ToList());
|
||||
var chain = tree.ToEvidenceChain(items
|
||||
.OrderBy(i => i.Digest, StringComparer.Ordinal)
|
||||
.ThenBy(i => i.Type, StringComparer.Ordinal)
|
||||
.ThenBy(i => i.Uri ?? string.Empty, StringComparer.Ordinal)
|
||||
.ThenBy(i => i.Description ?? string.Empty, StringComparer.Ordinal)
|
||||
.ThenBy(i => i.CollectedAt?.ToUniversalTime())
|
||||
.ToList());
|
||||
|
||||
// Act
|
||||
var valid = _builder.ValidateChain(chain);
|
||||
@@ -314,7 +338,13 @@ public class TrustEvidenceMerkleBuilderTests
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var proof = tree.GenerateProof(i);
|
||||
var sortedItems = items.OrderBy(x => x.Digest).ToList();
|
||||
var sortedItems = items
|
||||
.OrderBy(x => x.Digest, StringComparer.Ordinal)
|
||||
.ThenBy(x => x.Type, StringComparer.Ordinal)
|
||||
.ThenBy(x => x.Uri ?? string.Empty, StringComparer.Ordinal)
|
||||
.ThenBy(x => x.Description ?? string.Empty, StringComparer.Ordinal)
|
||||
.ThenBy(x => x.CollectedAt?.ToUniversalTime())
|
||||
.ToList();
|
||||
var valid = _builder.VerifyProof(sortedItems[i], proof, tree.Root);
|
||||
valid.Should().BeTrue($"proof for index {i} should be valid");
|
||||
}
|
||||
@@ -342,14 +372,19 @@ public class TrustEvidenceMerkleBuilderTests
|
||||
private static TrustEvidenceItem CreateEvidenceItem(
|
||||
string digest,
|
||||
string type = TrustEvidenceTypes.VexDocument,
|
||||
string? uri = null)
|
||||
string? uri = null,
|
||||
string? description = null,
|
||||
DateTimeOffset? collectedAt = null)
|
||||
{
|
||||
return new TrustEvidenceItem
|
||||
{
|
||||
Type = type,
|
||||
Digest = digest,
|
||||
Uri = uri,
|
||||
CollectedAt = new DateTimeOffset(2025, 1, 15, 12, 0, 0, TimeSpan.Zero)
|
||||
Description = description,
|
||||
CollectedAt = collectedAt ?? new DateTimeOffset(2025, 1, 15, 12, 0, 0, TimeSpan.Zero)
|
||||
};
|
||||
}
|
||||
|
||||
private static string ToDigestString(byte[] hash) => $"sha256:{Convert.ToHexStringLower(hash)}";
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
// Part of SPRINT_1227_0004_0004: Signed TrustVerdict Attestations
|
||||
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Attestor.TrustVerdict.Caching;
|
||||
using StellaOps.Attestor.TrustVerdict.Predicates;
|
||||
using System.Reflection;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Attestor.TrustVerdict.Tests;
|
||||
@@ -101,6 +103,47 @@ public class TrustVerdictCacheTests
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Get_UpdatesHitCountInCache()
|
||||
{
|
||||
// Arrange
|
||||
var entry = CreateCacheEntry("sha256:verdict1", "sha256:vex1", "tenant1");
|
||||
await _cache.SetAsync(entry);
|
||||
|
||||
// Act
|
||||
var first = await _cache.GetAsync("sha256:verdict1");
|
||||
var second = await _cache.GetAsync("sha256:verdict1");
|
||||
|
||||
// Assert
|
||||
first.Should().NotBeNull();
|
||||
second.Should().NotBeNull();
|
||||
first!.HitCount.Should().Be(1);
|
||||
second!.HitCount.Should().Be(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByVexDigest_RemovesExpiredIndex()
|
||||
{
|
||||
// Arrange
|
||||
var entry = CreateCacheEntry(
|
||||
"sha256:verdict1",
|
||||
"sha256:vex1",
|
||||
"tenant1",
|
||||
expiresAt: _timeProvider.GetUtcNow().AddMinutes(1));
|
||||
await _cache.SetAsync(entry);
|
||||
|
||||
_timeProvider.Advance(TimeSpan.FromMinutes(2));
|
||||
|
||||
// Act
|
||||
var result = await _cache.GetByVexDigestAsync("sha256:vex1", "tenant1");
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
|
||||
var index = GetVexIndex(_cache);
|
||||
index.Should().BeEmpty("expired entries should evict the VEX index mapping");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Invalidate_RemovesEntry()
|
||||
{
|
||||
@@ -151,6 +194,27 @@ public class TrustVerdictCacheTests
|
||||
results.Should().NotContainKey("sha256:vex4");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetBatch_RemovesExpiredEntries()
|
||||
{
|
||||
// Arrange
|
||||
await _cache.SetAsync(CreateCacheEntry(
|
||||
"sha256:v1",
|
||||
"sha256:vex1",
|
||||
"tenant1",
|
||||
expiresAt: _timeProvider.GetUtcNow().AddMinutes(1)));
|
||||
|
||||
_timeProvider.Advance(TimeSpan.FromMinutes(2));
|
||||
|
||||
// Act
|
||||
var results = await _cache.GetBatchAsync(["sha256:vex1"], "tenant1");
|
||||
|
||||
// Assert
|
||||
results.Should().BeEmpty();
|
||||
var index = GetVexIndex(_cache);
|
||||
index.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Set_EvictsOldestWhenFull()
|
||||
{
|
||||
@@ -230,6 +294,21 @@ public class TrustVerdictCacheTests
|
||||
result!.Predicate.Composite.Score.Should().Be(0.99m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValkeyCache_WhenEnabled_ThrowsNotSupported()
|
||||
{
|
||||
// Arrange
|
||||
var options = CreateOptions(new TrustVerdictCacheOptions { UseValkey = true });
|
||||
var cache = new ValkeyTrustVerdictCache(options, NullLogger<ValkeyTrustVerdictCache>.Instance, _timeProvider);
|
||||
|
||||
// Act
|
||||
Func<Task> act = () => cache.GetAsync("sha256:verdict1");
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<NotSupportedException>()
|
||||
.WithMessage("*TrustVerdictCache:UseValkey*");
|
||||
}
|
||||
|
||||
private TrustVerdictCacheEntry CreateCacheEntry(
|
||||
string verdictDigest,
|
||||
string vexDigest,
|
||||
@@ -297,4 +376,12 @@ public class TrustVerdictCacheTests
|
||||
monitor.Setup(m => m.CurrentValue).Returns(options);
|
||||
return monitor.Object;
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> GetVexIndex(InMemoryTrustVerdictCache cache)
|
||||
{
|
||||
var field = typeof(InMemoryTrustVerdictCache)
|
||||
.GetField("_vexToVerdictIndex", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
field.Should().NotBeNull("vex index should exist");
|
||||
return (Dictionary<string, string>)field!.GetValue(cache)!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
// TrustVerdictOciAttacherTests - Tests for OCI attacher stub behavior
|
||||
// Part of SPRINT_1227_0004_0004: Signed TrustVerdict Attestations
|
||||
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Attestor.TrustVerdict.Oci;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Attestor.TrustVerdict.Tests;
|
||||
|
||||
public class TrustVerdictOciAttacherTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Attach_WhenDisabled_ReturnsFailure()
|
||||
{
|
||||
var options = CreateOptions(new TrustVerdictOciOptions { Enabled = false });
|
||||
var attacher = new TrustVerdictOciAttacher(options, NullLogger<TrustVerdictOciAttacher>.Instance);
|
||||
|
||||
var result = await attacher.AttachAsync("registry.example/repo:tag", "ZXhhbXBsZQ==", "sha256:verdict");
|
||||
|
||||
result.Success.Should().BeFalse();
|
||||
result.ErrorMessage.Should().Be("OCI attachment is disabled");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Attach_InvalidReference_ReturnsFailure()
|
||||
{
|
||||
var options = CreateOptions(new TrustVerdictOciOptions { Enabled = true, DefaultRegistry = "registry.example" });
|
||||
var attacher = new TrustVerdictOciAttacher(options, NullLogger<TrustVerdictOciAttacher>.Instance);
|
||||
|
||||
var result = await attacher.AttachAsync("not-a-ref", "ZXhhbXBsZQ==", "sha256:verdict");
|
||||
|
||||
result.Success.Should().BeFalse();
|
||||
result.ErrorMessage.Should().StartWith("Invalid OCI reference:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Attach_WhenEnabled_ReturnsNotImplemented()
|
||||
{
|
||||
var options = CreateOptions(new TrustVerdictOciOptions { Enabled = true, DefaultRegistry = "registry.example" });
|
||||
var timeProvider = new FakeTimeProvider();
|
||||
var attacher = new TrustVerdictOciAttacher(options, NullLogger<TrustVerdictOciAttacher>.Instance, timeProvider: timeProvider);
|
||||
|
||||
var result = await attacher.AttachAsync("repo:tag", "ZXhhbXBsZQ==", "sha256:verdict");
|
||||
|
||||
result.Success.Should().BeFalse();
|
||||
result.ErrorMessage.Should().Be("OCI attachment is not implemented.");
|
||||
result.Duration.Should().BeGreaterThanOrEqualTo(TimeSpan.Zero);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Fetch_InvalidReference_ReturnsNull()
|
||||
{
|
||||
var options = CreateOptions(new TrustVerdictOciOptions { Enabled = true, DefaultRegistry = "registry.example" });
|
||||
var attacher = new TrustVerdictOciAttacher(options, NullLogger<TrustVerdictOciAttacher>.Instance);
|
||||
|
||||
var result = await attacher.FetchAsync("not-a-ref");
|
||||
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task List_InvalidReference_ReturnsEmpty()
|
||||
{
|
||||
var options = CreateOptions(new TrustVerdictOciOptions { Enabled = true, DefaultRegistry = "registry.example" });
|
||||
var attacher = new TrustVerdictOciAttacher(options, NullLogger<TrustVerdictOciAttacher>.Instance);
|
||||
|
||||
var result = await attacher.ListAsync("not-a-ref");
|
||||
|
||||
result.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Detach_WhenEnabled_ReturnsFalse()
|
||||
{
|
||||
var options = CreateOptions(new TrustVerdictOciOptions { Enabled = true, DefaultRegistry = "registry.example" });
|
||||
var attacher = new TrustVerdictOciAttacher(options, NullLogger<TrustVerdictOciAttacher>.Instance);
|
||||
|
||||
var result = await attacher.DetachAsync("repo:tag", "sha256:verdict");
|
||||
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
private static IOptionsMonitor<TrustVerdictOciOptions> CreateOptions(TrustVerdictOciOptions options)
|
||||
{
|
||||
var monitor = new Moq.Mock<IOptionsMonitor<TrustVerdictOciOptions>>();
|
||||
monitor.Setup(m => m.CurrentValue).Returns(options);
|
||||
return monitor.Object;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// TrustVerdictRepositoryMappingTests - Repository mapping tests
|
||||
// Part of SPRINT_1227_0004_0004: Signed TrustVerdict Attestations
|
||||
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Attestor.TrustVerdict.Persistence;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Attestor.TrustVerdict.Tests;
|
||||
|
||||
public class TrustVerdictRepositoryMappingTests
|
||||
{
|
||||
[Fact]
|
||||
public void ReadEntity_PreservesDateTimeOffset()
|
||||
{
|
||||
var issuedAt = new DateTimeOffset(2025, 2, 10, 8, 30, 0, TimeSpan.FromHours(2));
|
||||
var evaluatedAt = new DateTimeOffset(2025, 2, 11, 9, 45, 0, TimeSpan.FromHours(-5));
|
||||
var createdAt = new DateTimeOffset(2025, 2, 12, 10, 15, 0, TimeSpan.FromHours(1));
|
||||
var expiresAt = new DateTimeOffset(2025, 2, 13, 11, 0, 0, TimeSpan.Zero);
|
||||
|
||||
using var reader = CreateReader(issuedAt, evaluatedAt, createdAt, expiresAt);
|
||||
reader.Read();
|
||||
|
||||
var entity = PostgresTrustVerdictRepository.ReadEntity(reader);
|
||||
|
||||
entity.FreshnessIssuedAt.Should().Be(issuedAt);
|
||||
entity.EvaluatedAt.Should().Be(evaluatedAt);
|
||||
entity.CreatedAt.Should().Be(createdAt);
|
||||
entity.ExpiresAt.Should().Be(expiresAt);
|
||||
}
|
||||
|
||||
private static DbDataReader CreateReader(
|
||||
DateTimeOffset issuedAt,
|
||||
DateTimeOffset evaluatedAt,
|
||||
DateTimeOffset createdAt,
|
||||
DateTimeOffset expiresAt)
|
||||
{
|
||||
var table = new DataTable();
|
||||
table.Columns.Add("verdict_id", typeof(string));
|
||||
table.Columns.Add("tenant_id", typeof(Guid));
|
||||
table.Columns.Add("vex_digest", typeof(string));
|
||||
table.Columns.Add("vex_format", typeof(string));
|
||||
table.Columns.Add("provider_id", typeof(string));
|
||||
table.Columns.Add("statement_id", typeof(string));
|
||||
table.Columns.Add("vulnerability_id", typeof(string));
|
||||
table.Columns.Add("product_key", typeof(string));
|
||||
table.Columns.Add("vex_status", typeof(string));
|
||||
table.Columns.Add("origin_valid", typeof(bool));
|
||||
table.Columns.Add("origin_method", typeof(string));
|
||||
table.Columns.Add("origin_key_id", typeof(string));
|
||||
table.Columns.Add("origin_issuer_id", typeof(string));
|
||||
table.Columns.Add("origin_issuer_name", typeof(string));
|
||||
table.Columns.Add("origin_rekor_log_index", typeof(long));
|
||||
table.Columns.Add("origin_score", typeof(decimal));
|
||||
table.Columns.Add("freshness_status", typeof(string));
|
||||
table.Columns.Add("freshness_issued_at", typeof(DateTimeOffset));
|
||||
table.Columns.Add("freshness_expires_at", typeof(DateTimeOffset));
|
||||
table.Columns.Add("freshness_superseded_by", typeof(string));
|
||||
table.Columns.Add("freshness_age_days", typeof(int));
|
||||
table.Columns.Add("freshness_score", typeof(decimal));
|
||||
table.Columns.Add("reputation_composite", typeof(decimal));
|
||||
table.Columns.Add("reputation_authority", typeof(decimal));
|
||||
table.Columns.Add("reputation_accuracy", typeof(decimal));
|
||||
table.Columns.Add("reputation_timeliness", typeof(decimal));
|
||||
table.Columns.Add("reputation_coverage", typeof(decimal));
|
||||
table.Columns.Add("reputation_verification", typeof(decimal));
|
||||
table.Columns.Add("reputation_sample_count", typeof(int));
|
||||
table.Columns.Add("trust_score", typeof(decimal));
|
||||
table.Columns.Add("trust_tier", typeof(string));
|
||||
table.Columns.Add("trust_formula", typeof(string));
|
||||
table.Columns.Add("trust_reasons", typeof(string[]));
|
||||
table.Columns.Add("meets_policy_threshold", typeof(bool));
|
||||
table.Columns.Add("policy_threshold", typeof(decimal));
|
||||
table.Columns.Add("evidence_merkle_root", typeof(string));
|
||||
table.Columns.Add("evidence_items_json", typeof(string));
|
||||
table.Columns.Add("envelope_base64", typeof(string));
|
||||
table.Columns.Add("verdict_digest", typeof(string));
|
||||
table.Columns.Add("evaluated_at", typeof(DateTimeOffset));
|
||||
table.Columns.Add("evaluator_version", typeof(string));
|
||||
table.Columns.Add("crypto_profile", typeof(string));
|
||||
table.Columns.Add("policy_digest", typeof(string));
|
||||
table.Columns.Add("environment", typeof(string));
|
||||
table.Columns.Add("correlation_id", typeof(string));
|
||||
table.Columns.Add("oci_digest", typeof(string));
|
||||
table.Columns.Add("rekor_log_index", typeof(long));
|
||||
table.Columns.Add("created_at", typeof(DateTimeOffset));
|
||||
table.Columns.Add("expires_at", typeof(DateTimeOffset));
|
||||
|
||||
var row = table.NewRow();
|
||||
row["verdict_id"] = "verdict-1";
|
||||
row["tenant_id"] = Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
|
||||
row["vex_digest"] = "sha256:vex";
|
||||
row["vex_format"] = "openvex";
|
||||
row["provider_id"] = "provider";
|
||||
row["statement_id"] = "statement";
|
||||
row["vulnerability_id"] = "CVE-2025-1234";
|
||||
row["product_key"] = "pkg:npm/test@1.0.0";
|
||||
row["vex_status"] = DBNull.Value;
|
||||
row["origin_valid"] = true;
|
||||
row["origin_method"] = "dsse";
|
||||
row["origin_key_id"] = DBNull.Value;
|
||||
row["origin_issuer_id"] = DBNull.Value;
|
||||
row["origin_issuer_name"] = DBNull.Value;
|
||||
row["origin_rekor_log_index"] = DBNull.Value;
|
||||
row["origin_score"] = 0.9m;
|
||||
row["freshness_status"] = "fresh";
|
||||
row["freshness_issued_at"] = issuedAt;
|
||||
row["freshness_expires_at"] = DBNull.Value;
|
||||
row["freshness_superseded_by"] = DBNull.Value;
|
||||
row["freshness_age_days"] = 0;
|
||||
row["freshness_score"] = 1.0m;
|
||||
row["reputation_composite"] = 0.8m;
|
||||
row["reputation_authority"] = 0.8m;
|
||||
row["reputation_accuracy"] = 0.8m;
|
||||
row["reputation_timeliness"] = 0.8m;
|
||||
row["reputation_coverage"] = 0.8m;
|
||||
row["reputation_verification"] = 0.8m;
|
||||
row["reputation_sample_count"] = 10;
|
||||
row["trust_score"] = 0.9m;
|
||||
row["trust_tier"] = "High";
|
||||
row["trust_formula"] = "test";
|
||||
row["trust_reasons"] = new[] { "reason" };
|
||||
row["meets_policy_threshold"] = DBNull.Value;
|
||||
row["policy_threshold"] = DBNull.Value;
|
||||
row["evidence_merkle_root"] = "sha256:root";
|
||||
row["evidence_items_json"] = "[]";
|
||||
row["envelope_base64"] = DBNull.Value;
|
||||
row["verdict_digest"] = "sha256:verdict";
|
||||
row["evaluated_at"] = evaluatedAt;
|
||||
row["evaluator_version"] = "1.0.0";
|
||||
row["crypto_profile"] = "world";
|
||||
row["policy_digest"] = DBNull.Value;
|
||||
row["environment"] = DBNull.Value;
|
||||
row["correlation_id"] = DBNull.Value;
|
||||
row["oci_digest"] = DBNull.Value;
|
||||
row["rekor_log_index"] = DBNull.Value;
|
||||
row["created_at"] = createdAt;
|
||||
row["expires_at"] = expiresAt;
|
||||
|
||||
table.Rows.Add(row);
|
||||
return table.CreateDataReader();
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
// TrustVerdictServiceTests - Unit tests for TrustVerdictService
|
||||
// Part of SPRINT_1227_0004_0004: Signed TrustVerdict Attestations
|
||||
|
||||
using System.Globalization;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Attestor.TrustVerdict.Evidence;
|
||||
using StellaOps.Attestor.TrustVerdict.Predicates;
|
||||
using StellaOps.Attestor.TrustVerdict.Services;
|
||||
using Xunit;
|
||||
@@ -16,12 +18,14 @@ public class TrustVerdictServiceTests
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly IOptionsMonitor<TrustVerdictServiceOptions> _options;
|
||||
private readonly TrustVerdictService _service;
|
||||
private readonly ITrustEvidenceMerkleBuilder _merkleBuilder;
|
||||
|
||||
public TrustVerdictServiceTests()
|
||||
{
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 1, 15, 12, 0, 0, TimeSpan.Zero));
|
||||
_options = CreateOptions(new TrustVerdictServiceOptions { EvaluatorVersion = "1.0.0-test" });
|
||||
_service = new TrustVerdictService(_options, NullLogger<TrustVerdictService>.Instance, _timeProvider);
|
||||
_merkleBuilder = new TrustEvidenceMerkleBuilder();
|
||||
_service = new TrustVerdictService(_options, NullLogger<TrustVerdictService>.Instance, _merkleBuilder, _timeProvider);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -327,6 +331,131 @@ public class TrustVerdictServiceTests
|
||||
digests.Should().BeInAscendingOrder();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateVerdictAsync_EvidenceOrderingUsesTieBreakers()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateValidRequest() with
|
||||
{
|
||||
EvidenceItems =
|
||||
[
|
||||
new TrustVerdictEvidenceInput
|
||||
{
|
||||
Type = TrustEvidenceTypes.Signature,
|
||||
Digest = "sha256:dup",
|
||||
Uri = "https://example.com/b",
|
||||
Description = "b",
|
||||
CollectedAt = _timeProvider.GetUtcNow().AddMinutes(2)
|
||||
},
|
||||
new TrustVerdictEvidenceInput
|
||||
{
|
||||
Type = TrustEvidenceTypes.Certificate,
|
||||
Digest = "sha256:dup",
|
||||
Uri = "https://example.com/a",
|
||||
Description = "a",
|
||||
CollectedAt = _timeProvider.GetUtcNow().AddMinutes(1)
|
||||
},
|
||||
new TrustVerdictEvidenceInput
|
||||
{
|
||||
Type = TrustEvidenceTypes.VexDocument,
|
||||
Digest = "sha256:dup",
|
||||
Uri = "https://example.com/a",
|
||||
Description = "c",
|
||||
CollectedAt = _timeProvider.GetUtcNow().AddMinutes(3)
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _service.GenerateVerdictAsync(request);
|
||||
|
||||
// Assert
|
||||
var ordered = result.Predicate!.Evidence.Items
|
||||
.OrderBy(i => i.Digest, StringComparer.Ordinal)
|
||||
.ThenBy(i => i.Type, StringComparer.Ordinal)
|
||||
.ThenBy(i => i.Uri ?? string.Empty, StringComparer.Ordinal)
|
||||
.ThenBy(i => i.Description ?? string.Empty, StringComparer.Ordinal)
|
||||
.ThenBy(i => i.CollectedAt?.ToUniversalTime())
|
||||
.ToList();
|
||||
|
||||
result.Predicate.Evidence.Items.Should().BeEquivalentTo(ordered, opts => opts.WithStrictOrdering());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateVerdictAsync_EvidenceMerkleRootMatchesBuilder()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateValidRequest() with
|
||||
{
|
||||
EvidenceItems =
|
||||
[
|
||||
new TrustVerdictEvidenceInput
|
||||
{
|
||||
Type = TrustEvidenceTypes.VexDocument,
|
||||
Digest = "sha256:vex123",
|
||||
Uri = "https://example.com/vex/123",
|
||||
Description = "vex"
|
||||
},
|
||||
new TrustVerdictEvidenceInput
|
||||
{
|
||||
Type = TrustEvidenceTypes.Signature,
|
||||
Digest = "sha256:sig456",
|
||||
Description = "signature"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _service.GenerateVerdictAsync(request);
|
||||
|
||||
// Assert
|
||||
var tree = _merkleBuilder.Build(result.Predicate!.Evidence.Items);
|
||||
result.Predicate.Evidence.MerkleRoot.Should().Be(tree.Root);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateVerdictAsync_UsesInvariantCultureForReasons()
|
||||
{
|
||||
var originalCulture = CultureInfo.CurrentCulture;
|
||||
var originalUiCulture = CultureInfo.CurrentUICulture;
|
||||
try
|
||||
{
|
||||
CultureInfo.CurrentCulture = new CultureInfo("fr-FR");
|
||||
CultureInfo.CurrentUICulture = new CultureInfo("fr-FR");
|
||||
|
||||
var request = CreateValidRequest() with
|
||||
{
|
||||
Origin = new TrustVerdictOriginInput { Valid = true, Method = VerificationMethods.Dsse },
|
||||
Freshness = new TrustVerdictFreshnessInput
|
||||
{
|
||||
Status = FreshnessStatuses.Fresh,
|
||||
IssuedAt = _timeProvider.GetUtcNow()
|
||||
},
|
||||
Reputation = new TrustVerdictReputationInput
|
||||
{
|
||||
Authority = 1.0m,
|
||||
Accuracy = 1.0m,
|
||||
Timeliness = 1.0m,
|
||||
Coverage = 1.0m,
|
||||
Verification = 1.0m,
|
||||
ComputedAt = _timeProvider.GetUtcNow(),
|
||||
SampleCount = 10
|
||||
}
|
||||
};
|
||||
|
||||
var result = await _service.GenerateVerdictAsync(request);
|
||||
|
||||
var reasons = result.Predicate!.Composite.Reasons;
|
||||
reasons.Should().Contain(r => r.Contains("100%", StringComparison.Ordinal));
|
||||
reasons.Should().NotContain(r => r.Contains("100 %", StringComparison.Ordinal));
|
||||
}
|
||||
finally
|
||||
{
|
||||
CultureInfo.CurrentCulture = originalCulture;
|
||||
CultureInfo.CurrentUICulture = originalUiCulture;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateVerdictAsync_ChecksPolicyThreshold()
|
||||
{
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// TrustVerdictCache - Valkey-backed cache for TrustVerdict lookups
|
||||
// Part of SPRINT_1227_0004_0004: Signed TrustVerdict Attestations
|
||||
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Attestor.TrustVerdict.Predicates;
|
||||
@@ -164,11 +163,13 @@ public sealed class InMemoryTrustVerdictCache : ITrustVerdictCache
|
||||
if (_timeProvider.GetUtcNow() < entry.ExpiresAt)
|
||||
{
|
||||
Interlocked.Increment(ref _hitCount);
|
||||
return Task.FromResult<TrustVerdictCacheEntry?>(entry with { HitCount = entry.HitCount + 1 });
|
||||
var updated = entry with { HitCount = entry.HitCount + 1 };
|
||||
_byVerdictDigest[verdictDigest] = updated;
|
||||
return Task.FromResult<TrustVerdictCacheEntry?>(updated);
|
||||
}
|
||||
|
||||
// Expired, remove
|
||||
_byVerdictDigest.Remove(verdictDigest);
|
||||
RemoveEntryLocked(entry);
|
||||
Interlocked.Increment(ref _evictionCount);
|
||||
}
|
||||
|
||||
@@ -188,7 +189,25 @@ public sealed class InMemoryTrustVerdictCache : ITrustVerdictCache
|
||||
{
|
||||
if (_vexToVerdictIndex.TryGetValue(key, out var verdictDigest))
|
||||
{
|
||||
return GetAsync(verdictDigest, ct);
|
||||
if (!_byVerdictDigest.TryGetValue(verdictDigest, out var entry))
|
||||
{
|
||||
_vexToVerdictIndex.Remove(key);
|
||||
Interlocked.Increment(ref _missCount);
|
||||
return Task.FromResult<TrustVerdictCacheEntry?>(null);
|
||||
}
|
||||
|
||||
if (_timeProvider.GetUtcNow() < entry.ExpiresAt)
|
||||
{
|
||||
Interlocked.Increment(ref _hitCount);
|
||||
var updated = entry with { HitCount = entry.HitCount + 1 };
|
||||
_byVerdictDigest[verdictDigest] = updated;
|
||||
return Task.FromResult<TrustVerdictCacheEntry?>(updated);
|
||||
}
|
||||
|
||||
RemoveEntryLocked(entry);
|
||||
Interlocked.Increment(ref _evictionCount);
|
||||
Interlocked.Increment(ref _missCount);
|
||||
return Task.FromResult<TrustVerdictCacheEntry?>(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,15 +286,30 @@ public sealed class InMemoryTrustVerdictCache : ITrustVerdictCache
|
||||
{
|
||||
var vexKey = BuildVexKey(vexDigest, tenantId);
|
||||
|
||||
if (_vexToVerdictIndex.TryGetValue(vexKey, out var verdictDigest) &&
|
||||
_byVerdictDigest.TryGetValue(verdictDigest, out var entry) &&
|
||||
now < entry.ExpiresAt)
|
||||
if (!_vexToVerdictIndex.TryGetValue(vexKey, out var verdictDigest))
|
||||
{
|
||||
Interlocked.Increment(ref _missCount);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_byVerdictDigest.TryGetValue(verdictDigest, out var entry))
|
||||
{
|
||||
_vexToVerdictIndex.Remove(vexKey);
|
||||
Interlocked.Increment(ref _missCount);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (now < entry.ExpiresAt)
|
||||
{
|
||||
results[vexDigest] = entry;
|
||||
Interlocked.Increment(ref _hitCount);
|
||||
var updated = entry with { HitCount = entry.HitCount + 1 };
|
||||
_byVerdictDigest[verdictDigest] = updated;
|
||||
results[vexDigest] = updated;
|
||||
}
|
||||
else
|
||||
{
|
||||
RemoveEntryLocked(entry);
|
||||
Interlocked.Increment(ref _evictionCount);
|
||||
Interlocked.Increment(ref _missCount);
|
||||
}
|
||||
}
|
||||
@@ -312,13 +346,18 @@ public sealed class InMemoryTrustVerdictCache : ITrustVerdictCache
|
||||
|
||||
if (oldest != null)
|
||||
{
|
||||
_byVerdictDigest.Remove(oldest.VerdictDigest);
|
||||
var vexKey = BuildVexKey(oldest.VexDigest, oldest.TenantId);
|
||||
_vexToVerdictIndex.Remove(vexKey);
|
||||
RemoveEntryLocked(oldest);
|
||||
Interlocked.Increment(ref _evictionCount);
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveEntryLocked(TrustVerdictCacheEntry entry)
|
||||
{
|
||||
_byVerdictDigest.Remove(entry.VerdictDigest);
|
||||
var vexKey = BuildVexKey(entry.VexDigest, entry.TenantId);
|
||||
_vexToVerdictIndex.Remove(vexKey);
|
||||
}
|
||||
|
||||
private long EstimateMemoryUsage()
|
||||
{
|
||||
// Rough estimate: ~1KB per entry average
|
||||
@@ -334,7 +373,6 @@ public sealed class ValkeyTrustVerdictCache : ITrustVerdictCache, IAsyncDisposab
|
||||
private readonly IOptionsMonitor<TrustVerdictCacheOptions> _options;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly ILogger<ValkeyTrustVerdictCache> _logger;
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
|
||||
// Note: In production, this would use StackExchange.Redis or similar Valkey client
|
||||
// For now, we delegate to in-memory as a fallback
|
||||
@@ -349,11 +387,6 @@ public sealed class ValkeyTrustVerdictCache : ITrustVerdictCache, IAsyncDisposab
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
|
||||
_jsonOptions = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
_fallback = new InMemoryTrustVerdictCache(options, timeProvider);
|
||||
}
|
||||
|
||||
@@ -366,21 +399,8 @@ public sealed class ValkeyTrustVerdictCache : ITrustVerdictCache, IAsyncDisposab
|
||||
return await _fallback.GetAsync(verdictDigest, ct);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// TODO: Implement Valkey lookup
|
||||
// var key = BuildKey(opts.KeyPrefix, "verdict", verdictDigest);
|
||||
// var value = await _valkeyClient.GetAsync(key);
|
||||
// if (value != null)
|
||||
// return JsonSerializer.Deserialize<TrustVerdictCacheEntry>(value, _jsonOptions);
|
||||
|
||||
return await _fallback.GetAsync(verdictDigest, ct);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Valkey lookup failed for {Digest}, falling back to in-memory", verdictDigest);
|
||||
return await _fallback.GetAsync(verdictDigest, ct);
|
||||
}
|
||||
ThrowValkeyNotImplemented();
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<TrustVerdictCacheEntry?> GetByVexDigestAsync(
|
||||
@@ -395,89 +415,45 @@ public sealed class ValkeyTrustVerdictCache : ITrustVerdictCache, IAsyncDisposab
|
||||
return await _fallback.GetByVexDigestAsync(vexDigest, tenantId, ct);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// TODO: Implement Valkey lookup via secondary index
|
||||
return await _fallback.GetByVexDigestAsync(vexDigest, tenantId, ct);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Valkey lookup failed for VEX {Digest}, falling back", vexDigest);
|
||||
return await _fallback.GetByVexDigestAsync(vexDigest, tenantId, ct);
|
||||
}
|
||||
ThrowValkeyNotImplemented();
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task SetAsync(TrustVerdictCacheEntry entry, CancellationToken ct = default)
|
||||
{
|
||||
var opts = _options.CurrentValue;
|
||||
|
||||
// Always set in fallback for local consistency
|
||||
await _fallback.SetAsync(entry, ct);
|
||||
|
||||
if (!opts.UseValkey)
|
||||
{
|
||||
await _fallback.SetAsync(entry, ct);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// TODO: Implement Valkey SET with TTL
|
||||
// var key = BuildKey(opts.KeyPrefix, "verdict", entry.VerdictDigest);
|
||||
// var value = JsonSerializer.Serialize(entry, _jsonOptions);
|
||||
// await _valkeyClient.SetAsync(key, value, opts.DefaultTtl);
|
||||
|
||||
// Also set secondary index
|
||||
// var vexKey = BuildKey(opts.KeyPrefix, "vex", entry.TenantId, entry.VexDigest);
|
||||
// await _valkeyClient.SetAsync(vexKey, entry.VerdictDigest, opts.DefaultTtl);
|
||||
|
||||
_logger.LogDebug("Cached verdict {Digest} in Valkey", entry.VerdictDigest);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to cache verdict {Digest} in Valkey", entry.VerdictDigest);
|
||||
}
|
||||
ThrowValkeyNotImplemented();
|
||||
}
|
||||
|
||||
public async Task InvalidateAsync(string verdictDigest, CancellationToken ct = default)
|
||||
{
|
||||
await _fallback.InvalidateAsync(verdictDigest, ct);
|
||||
|
||||
var opts = _options.CurrentValue;
|
||||
if (!opts.UseValkey)
|
||||
{
|
||||
await _fallback.InvalidateAsync(verdictDigest, ct);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// TODO: Implement Valkey DEL
|
||||
_logger.LogDebug("Invalidated verdict {Digest} in Valkey", verdictDigest);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to invalidate verdict {Digest} in Valkey", verdictDigest);
|
||||
}
|
||||
ThrowValkeyNotImplemented();
|
||||
}
|
||||
|
||||
public async Task InvalidateByVexDigestAsync(string vexDigest, string tenantId, CancellationToken ct = default)
|
||||
{
|
||||
await _fallback.InvalidateByVexDigestAsync(vexDigest, tenantId, ct);
|
||||
|
||||
var opts = _options.CurrentValue;
|
||||
if (!opts.UseValkey)
|
||||
{
|
||||
await _fallback.InvalidateByVexDigestAsync(vexDigest, tenantId, ct);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// TODO: Implement Valkey DEL via secondary index
|
||||
_logger.LogDebug("Invalidated verdicts for VEX {Digest} in Valkey", vexDigest);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to invalidate VEX {Digest} in Valkey", vexDigest);
|
||||
}
|
||||
ThrowValkeyNotImplemented();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyDictionary<string, TrustVerdictCacheEntry>> GetBatchAsync(
|
||||
@@ -492,21 +468,20 @@ public sealed class ValkeyTrustVerdictCache : ITrustVerdictCache, IAsyncDisposab
|
||||
return await _fallback.GetBatchAsync(vexDigests, tenantId, ct);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// TODO: Implement Valkey MGET for batch lookup
|
||||
return await _fallback.GetBatchAsync(vexDigests, tenantId, ct);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Valkey batch lookup failed, falling back");
|
||||
return await _fallback.GetBatchAsync(vexDigests, tenantId, ct);
|
||||
}
|
||||
ThrowValkeyNotImplemented();
|
||||
return new Dictionary<string, TrustVerdictCacheEntry>(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
public Task<TrustVerdictCacheStats> GetStatsAsync(CancellationToken ct = default)
|
||||
{
|
||||
// TODO: Combine Valkey INFO stats with fallback stats
|
||||
var opts = _options.CurrentValue;
|
||||
if (!opts.UseValkey)
|
||||
{
|
||||
return _fallback.GetStatsAsync(ct);
|
||||
}
|
||||
|
||||
ThrowValkeyNotImplemented();
|
||||
return _fallback.GetStatsAsync(ct);
|
||||
}
|
||||
|
||||
@@ -515,6 +490,15 @@ public sealed class ValkeyTrustVerdictCache : ITrustVerdictCache, IAsyncDisposab
|
||||
// TODO: Dispose Valkey client when implemented
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
private void ThrowValkeyNotImplemented()
|
||||
{
|
||||
_logger.LogError(
|
||||
"Valkey TrustVerdict cache is not implemented. Set {SectionKey}:UseValkey=false.",
|
||||
TrustVerdictCacheOptions.SectionKey);
|
||||
throw new NotSupportedException(
|
||||
"Valkey TrustVerdict cache is not implemented. Set TrustVerdictCache:UseValkey=false.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using StellaOps.Attestor.StandardPredicates;
|
||||
|
||||
namespace StellaOps.Attestor.TrustVerdict;
|
||||
|
||||
@@ -47,7 +47,7 @@ CREATE TABLE vex.trust_verdicts (
|
||||
|
||||
-- Trust composite
|
||||
trust_score DECIMAL(5,4) NOT NULL,
|
||||
trust_tier TEXT NOT NULL, -- verified, high, medium, low, untrusted
|
||||
trust_tier TEXT NOT NULL, -- VeryHigh, High, Medium, Low, VeryLow
|
||||
trust_formula TEXT NOT NULL,
|
||||
trust_reasons TEXT[] NOT NULL,
|
||||
meets_policy_threshold BOOLEAN,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// TrustVerdictRepository - PostgreSQL persistence for TrustVerdict attestations
|
||||
// Part of SPRINT_1227_0004_0004: Signed TrustVerdict Attestations
|
||||
|
||||
using System.Data.Common;
|
||||
using System.Text.Json;
|
||||
using Npgsql;
|
||||
using NpgsqlTypes;
|
||||
@@ -182,12 +183,12 @@ public sealed record TrustVerdictStats
|
||||
public sealed class PostgresTrustVerdictRepository : ITrustVerdictRepository
|
||||
{
|
||||
private readonly NpgsqlDataSource _dataSource;
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
private static readonly JsonSerializerOptions JsonOptions =
|
||||
new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
|
||||
|
||||
public PostgresTrustVerdictRepository(NpgsqlDataSource dataSource)
|
||||
{
|
||||
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
|
||||
_jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
|
||||
}
|
||||
|
||||
public async Task<string> StoreAsync(TrustVerdictEntity entity, CancellationToken ct = default)
|
||||
@@ -412,8 +413,8 @@ public sealed class PostgresTrustVerdictRepository : ITrustVerdictRepository
|
||||
ActiveCount = reader.GetInt64(1),
|
||||
ExpiredCount = reader.GetInt64(2),
|
||||
AverageScore = reader.GetDecimal(3),
|
||||
OldestEvaluation = reader.IsDBNull(4) ? null : reader.GetDateTime(4),
|
||||
NewestEvaluation = reader.IsDBNull(5) ? null : reader.GetDateTime(5),
|
||||
OldestEvaluation = reader.IsDBNull(4) ? null : reader.GetFieldValue<DateTimeOffset>(4),
|
||||
NewestEvaluation = reader.IsDBNull(5) ? null : reader.GetFieldValue<DateTimeOffset>(5),
|
||||
CountByTier = await GetCountByTierAsync(tenantId, ct),
|
||||
CountByProvider = await GetCountByProviderAsync(tenantId, ct)
|
||||
};
|
||||
@@ -532,7 +533,7 @@ public sealed class PostgresTrustVerdictRepository : ITrustVerdictRepository
|
||||
cmd.Parameters.AddWithValue("policy_threshold", entity.PolicyThreshold ?? (object)DBNull.Value);
|
||||
|
||||
cmd.Parameters.AddWithValue("evidence_merkle_root", entity.EvidenceMerkleRoot);
|
||||
cmd.Parameters.AddWithValue("evidence_items_json", JsonSerializer.Serialize(entity.EvidenceItems, _jsonOptions));
|
||||
cmd.Parameters.AddWithValue("evidence_items_json", JsonSerializer.Serialize(entity.EvidenceItems, JsonOptions));
|
||||
|
||||
cmd.Parameters.AddWithValue("envelope_base64", entity.EnvelopeBase64 ?? (object)DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("verdict_digest", entity.VerdictDigest);
|
||||
@@ -551,10 +552,10 @@ public sealed class PostgresTrustVerdictRepository : ITrustVerdictRepository
|
||||
cmd.Parameters.AddWithValue("expires_at", entity.ExpiresAt ?? (object)DBNull.Value);
|
||||
}
|
||||
|
||||
private TrustVerdictEntity ReadEntity(NpgsqlDataReader reader)
|
||||
internal static TrustVerdictEntity ReadEntity(DbDataReader reader)
|
||||
{
|
||||
var evidenceJson = reader.GetString(reader.GetOrdinal("evidence_items_json"));
|
||||
var evidenceItems = JsonSerializer.Deserialize<List<TrustEvidenceItem>>(evidenceJson, _jsonOptions) ?? [];
|
||||
var evidenceItems = JsonSerializer.Deserialize<List<TrustEvidenceItem>>(evidenceJson, JsonOptions) ?? [];
|
||||
|
||||
return new TrustVerdictEntity
|
||||
{
|
||||
@@ -578,8 +579,10 @@ public sealed class PostgresTrustVerdictRepository : ITrustVerdictRepository
|
||||
OriginScore = reader.GetDecimal(reader.GetOrdinal("origin_score")),
|
||||
|
||||
FreshnessStatus = reader.GetString(reader.GetOrdinal("freshness_status")),
|
||||
FreshnessIssuedAt = reader.GetDateTime(reader.GetOrdinal("freshness_issued_at")),
|
||||
FreshnessExpiresAt = reader.IsDBNull(reader.GetOrdinal("freshness_expires_at")) ? null : reader.GetDateTime(reader.GetOrdinal("freshness_expires_at")),
|
||||
FreshnessIssuedAt = reader.GetFieldValue<DateTimeOffset>(reader.GetOrdinal("freshness_issued_at")),
|
||||
FreshnessExpiresAt = reader.IsDBNull(reader.GetOrdinal("freshness_expires_at"))
|
||||
? null
|
||||
: reader.GetFieldValue<DateTimeOffset>(reader.GetOrdinal("freshness_expires_at")),
|
||||
FreshnessSupersededBy = reader.IsDBNull(reader.GetOrdinal("freshness_superseded_by")) ? null : reader.GetString(reader.GetOrdinal("freshness_superseded_by")),
|
||||
FreshnessAgeDays = reader.GetInt32(reader.GetOrdinal("freshness_age_days")),
|
||||
FreshnessScore = reader.GetDecimal(reader.GetOrdinal("freshness_score")),
|
||||
@@ -605,7 +608,7 @@ public sealed class PostgresTrustVerdictRepository : ITrustVerdictRepository
|
||||
EnvelopeBase64 = reader.IsDBNull(reader.GetOrdinal("envelope_base64")) ? null : reader.GetString(reader.GetOrdinal("envelope_base64")),
|
||||
VerdictDigest = reader.GetString(reader.GetOrdinal("verdict_digest")),
|
||||
|
||||
EvaluatedAt = reader.GetDateTime(reader.GetOrdinal("evaluated_at")),
|
||||
EvaluatedAt = reader.GetFieldValue<DateTimeOffset>(reader.GetOrdinal("evaluated_at")),
|
||||
EvaluatorVersion = reader.GetString(reader.GetOrdinal("evaluator_version")),
|
||||
CryptoProfile = reader.GetString(reader.GetOrdinal("crypto_profile")),
|
||||
PolicyDigest = reader.IsDBNull(reader.GetOrdinal("policy_digest")) ? null : reader.GetString(reader.GetOrdinal("policy_digest")),
|
||||
@@ -615,8 +618,10 @@ public sealed class PostgresTrustVerdictRepository : ITrustVerdictRepository
|
||||
OciDigest = reader.IsDBNull(reader.GetOrdinal("oci_digest")) ? null : reader.GetString(reader.GetOrdinal("oci_digest")),
|
||||
RekorLogIndex = reader.IsDBNull(reader.GetOrdinal("rekor_log_index")) ? null : reader.GetInt64(reader.GetOrdinal("rekor_log_index")),
|
||||
|
||||
CreatedAt = reader.GetDateTime(reader.GetOrdinal("created_at")),
|
||||
ExpiresAt = reader.IsDBNull(reader.GetOrdinal("expires_at")) ? null : reader.GetDateTime(reader.GetOrdinal("expires_at"))
|
||||
CreatedAt = reader.GetFieldValue<DateTimeOffset>(reader.GetOrdinal("created_at")),
|
||||
ExpiresAt = reader.IsDBNull(reader.GetOrdinal("expires_at"))
|
||||
? null
|
||||
: reader.GetFieldValue<DateTimeOffset>(reader.GetOrdinal("expires_at"))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,6 +162,7 @@ public sealed record TrustVerdictEvidenceInput
|
||||
public required string Digest { get; init; }
|
||||
public string? Uri { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public DateTimeOffset? CollectedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -451,7 +452,7 @@ public sealed class TrustVerdictService : ITrustVerdictService
|
||||
Digest = e.Digest,
|
||||
Uri = e.Uri,
|
||||
Description = e.Description,
|
||||
CollectedAt = evaluatedAt
|
||||
CollectedAt = e.CollectedAt ?? evaluatedAt
|
||||
})
|
||||
.ToList();
|
||||
|
||||
|
||||
@@ -24,4 +24,8 @@
|
||||
<ProjectReference Include="..\StellaOps.Attestor.StandardPredicates\StellaOps.Attestor.StandardPredicates.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="StellaOps.Attestor.TrustVerdict.Tests" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -7,4 +7,4 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0067-M | DONE | Maintainability audit for StellaOps.Attestor.TrustVerdict. |
|
||||
| AUDIT-0067-T | DONE | Test coverage audit for StellaOps.Attestor.TrustVerdict. |
|
||||
| AUDIT-0067-A | DOING | Applying audit fixes for TrustVerdict library. |
|
||||
| AUDIT-0067-A | DONE | Applied audit fixes for TrustVerdict library. |
|
||||
|
||||
Reference in New Issue
Block a user