save progress
This commit is contained in:
@@ -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