save progress

This commit is contained in:
StellaOps Bot
2026-01-03 11:02:24 +02:00
parent ca578801fd
commit 83c37243e0
446 changed files with 22798 additions and 4031 deletions

View File

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

View File

@@ -3,6 +3,7 @@
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using StellaOps.Attestor.StandardPredicates;
namespace StellaOps.Attestor.TrustVerdict;

View File

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

View File

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

View File

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

View File

@@ -24,4 +24,8 @@
<ProjectReference Include="..\StellaOps.Attestor.StandardPredicates\StellaOps.Attestor.StandardPredicates.csproj" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="StellaOps.Attestor.TrustVerdict.Tests" />
</ItemGroup>
</Project>

View File

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