save progress

This commit is contained in:
StellaOps Bot
2026-01-04 19:08:47 +02:00
parent f7d27c6fda
commit 75611a505f
97 changed files with 4531 additions and 293 deletions

View File

@@ -8,6 +8,7 @@ using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Npgsql;
using StellaOps.Determinism;
using StellaOps.VexLens.Consensus;
using StellaOps.VexLens.Models;
using StellaOps.VexLens.Storage;
@@ -26,16 +27,19 @@ public sealed class PostgresConsensusProjectionStore : IConsensusProjectionStore
private readonly IConsensusEventEmitter? _eventEmitter;
private readonly ILogger<PostgresConsensusProjectionStore> _logger;
private readonly TimeProvider _timeProvider;
private readonly IGuidProvider _guidProvider;
public PostgresConsensusProjectionStore(
NpgsqlDataSource dataSource,
ILogger<PostgresConsensusProjectionStore> logger,
TimeProvider? timeProvider = null,
IGuidProvider? guidProvider = null,
IConsensusEventEmitter? eventEmitter = null)
{
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_timeProvider = timeProvider ?? TimeProvider.System;
_guidProvider = guidProvider ?? new SystemGuidProvider();
_eventEmitter = eventEmitter;
}
@@ -52,7 +56,7 @@ public sealed class PostgresConsensusProjectionStore : IConsensusProjectionStore
activity?.SetTag("vulnerabilityId", result.VulnerabilityId);
activity?.SetTag("productKey", result.ProductKey);
var projectionId = Guid.NewGuid();
var projectionId = _guidProvider.NewGuid();
var now = _timeProvider.GetUtcNow();
// Check for previous projection to track history
@@ -527,7 +531,7 @@ public sealed class PostgresConsensusProjectionStore : IConsensusProjectionStore
// Always emit computed event
await _eventEmitter.EmitConsensusComputedAsync(
new ConsensusComputedEvent(
EventId: Guid.NewGuid().ToString(),
EventId: _guidProvider.NewGuid().ToString(),
ProjectionId: projection.ProjectionId,
VulnerabilityId: projection.VulnerabilityId,
ProductKey: projection.ProductKey,
@@ -546,7 +550,7 @@ public sealed class PostgresConsensusProjectionStore : IConsensusProjectionStore
{
await _eventEmitter.EmitStatusChangedAsync(
new ConsensusStatusChangedEvent(
EventId: Guid.NewGuid().ToString(),
EventId: _guidProvider.NewGuid().ToString(),
ProjectionId: projection.ProjectionId,
VulnerabilityId: projection.VulnerabilityId,
ProductKey: projection.ProductKey,
@@ -564,7 +568,7 @@ public sealed class PostgresConsensusProjectionStore : IConsensusProjectionStore
{
await _eventEmitter.EmitConflictDetectedAsync(
new ConsensusConflictDetectedEvent(
EventId: Guid.NewGuid().ToString(),
EventId: _guidProvider.NewGuid().ToString(),
ProjectionId: projection.ProjectionId,
VulnerabilityId: projection.VulnerabilityId,
ProductKey: projection.ProductKey,

View File

@@ -18,6 +18,7 @@
<ItemGroup>
<ProjectReference Include="..\StellaOps.VexLens\StellaOps.VexLens.csproj" />
<ProjectReference Include="..\..\__Libraries\StellaOps.Infrastructure.Postgres\StellaOps.Infrastructure.Postgres.csproj" />
<ProjectReference Include="..\..\__Libraries\StellaOps.Determinism.Abstractions\StellaOps.Determinism.Abstractions.csproj" />
</ItemGroup>
<ItemGroup>

View File

@@ -325,6 +325,7 @@ public static class VexLensEndpointExtensions
[FromQuery] DateTimeOffset? fromDate,
[FromQuery] DateTimeOffset? toDate,
[FromServices] IGatingStatisticsStore statsStore,
[FromServices] TimeProvider timeProvider,
HttpContext context,
CancellationToken cancellationToken)
{
@@ -340,7 +341,7 @@ public static class VexLensEndpointExtensions
TotalSurfaced: stats.TotalSurfaced,
TotalDamped: stats.TotalDamped,
AverageDampingPercent: stats.AverageDampingPercent,
ComputedAt: DateTimeOffset.UtcNow));
ComputedAt: timeProvider.GetUtcNow()));
}
private static async Task<IResult> GateSnapshotAsync(

View File

@@ -1,5 +1,6 @@
using System.Security.Cryptography;
using System.Text;
using StellaOps.Determinism;
using StellaOps.VexLens.Consensus;
using StellaOps.VexLens.Models;
using StellaOps.VexLens.Storage;
@@ -43,17 +44,20 @@ public sealed class ConsensusRationaleService : IConsensusRationaleService
private readonly IConsensusProjectionStore _projectionStore;
private readonly IVexConsensusEngine _consensusEngine;
private readonly ITrustWeightEngine _trustWeightEngine;
private readonly IGuidProvider _guidProvider;
private const string AlgorithmVersion = "1.0.0";
public ConsensusRationaleService(
IConsensusProjectionStore projectionStore,
IVexConsensusEngine consensusEngine,
ITrustWeightEngine trustWeightEngine)
ITrustWeightEngine trustWeightEngine,
IGuidProvider? guidProvider = null)
{
_projectionStore = projectionStore;
_consensusEngine = consensusEngine;
_trustWeightEngine = trustWeightEngine;
_guidProvider = guidProvider ?? new SystemGuidProvider();
}
public async Task<GenerateRationaleResponse> GenerateRationaleAsync(
@@ -177,7 +181,7 @@ public sealed class ConsensusRationaleService : IConsensusRationaleService
var outputHash = ComputeOutputHash(result, contributions, conflicts);
var rationale = new DetailedConsensusRationale(
RationaleId: $"rat-{Guid.NewGuid():N}",
RationaleId: $"rat-{_guidProvider.NewGuid():N}",
VulnerabilityId: result.VulnerabilityId,
ProductKey: result.ProductKey,
ConsensusStatus: result.ConsensusStatus,

View File

@@ -137,19 +137,22 @@ public sealed class VexLensApiService : IVexLensApiService
private readonly IConsensusProjectionStore _projectionStore;
private readonly IIssuerDirectory _issuerDirectory;
private readonly IVexStatementProvider _statementProvider;
private readonly TimeProvider _timeProvider;
public VexLensApiService(
IVexConsensusEngine consensusEngine,
ITrustWeightEngine trustWeightEngine,
IConsensusProjectionStore projectionStore,
IIssuerDirectory issuerDirectory,
IVexStatementProvider statementProvider)
IVexStatementProvider statementProvider,
TimeProvider? timeProvider = null)
{
_consensusEngine = consensusEngine;
_trustWeightEngine = trustWeightEngine;
_projectionStore = projectionStore;
_issuerDirectory = issuerDirectory;
_statementProvider = statementProvider;
_timeProvider = timeProvider ?? TimeProvider.System;
}
public async Task<ComputeConsensusResponse> ComputeConsensusAsync(
@@ -164,7 +167,7 @@ public sealed class VexLensApiService : IVexLensApiService
cancellationToken);
// Compute trust weights
var now = DateTimeOffset.UtcNow;
var now = _timeProvider.GetUtcNow();
var weightedStatements = new List<WeightedStatement>();
foreach (var stmt in statements)
@@ -237,7 +240,7 @@ public sealed class VexLensApiService : IVexLensApiService
cancellationToken);
// Compute trust weights
var now = DateTimeOffset.UtcNow;
var now = _timeProvider.GetUtcNow();
var weightedStatements = new List<WeightedStatement>();
foreach (var stmt in statements)
@@ -293,7 +296,7 @@ public sealed class VexLensApiService : IVexLensApiService
var resolutionResult = await _consensusEngine.ComputeConsensusWithProofAsync(
consensusRequest,
proofContext,
TimeProvider.System,
_timeProvider,
cancellationToken);
// Store result if requested
@@ -348,7 +351,7 @@ public sealed class VexLensApiService : IVexLensApiService
TotalCount: request.Targets.Count,
SuccessCount: results.Count,
FailureCount: failures,
CompletedAt: DateTimeOffset.UtcNow);
CompletedAt: _timeProvider.GetUtcNow());
}
public async Task<ProjectionDetailResponse?> GetProjectionAsync(
@@ -452,7 +455,7 @@ public sealed class VexLensApiService : IVexLensApiService
var withConflicts = projections.Count(p => p.ConflictCount > 0);
var last24h = DateTimeOffset.UtcNow.AddDays(-1);
var last24h = _timeProvider.GetUtcNow().AddDays(-1);
var changesLast24h = projections.Count(p => p.StatusChanged && p.ComputedAt >= last24h);
return new ConsensusStatisticsResponse(
@@ -462,7 +465,7 @@ public sealed class VexLensApiService : IVexLensApiService
AverageConfidence: avgConfidence,
ProjectionsWithConflicts: withConflicts,
StatusChangesLast24h: changesLast24h,
ComputedAt: DateTimeOffset.UtcNow);
ComputedAt: _timeProvider.GetUtcNow());
}
public async Task<IssuerListResponse> ListIssuersAsync(

View File

@@ -472,15 +472,18 @@ public sealed class TrustScorecardApiService : ITrustScorecardApiService
private readonly ISourceTrustScoreCalculator _scoreCalculator;
private readonly IConflictAuditStore? _auditStore;
private readonly ITrustScoreHistoryStore? _historyStore;
private readonly TimeProvider _timeProvider;
public TrustScorecardApiService(
ISourceTrustScoreCalculator scoreCalculator,
IConflictAuditStore? auditStore = null,
ITrustScoreHistoryStore? historyStore = null)
ITrustScoreHistoryStore? historyStore = null,
TimeProvider? timeProvider = null)
{
_scoreCalculator = scoreCalculator;
_auditStore = auditStore;
_historyStore = historyStore;
_timeProvider = timeProvider ?? TimeProvider.System;
}
public async Task<TrustScorecardResponse> GetScorecardAsync(
@@ -544,7 +547,7 @@ public sealed class TrustScorecardApiService : ITrustScorecardApiService
SignatureValidityRate = cachedScore.Breakdown.Verification.SignatureValidityRate,
VerificationMethod = cachedScore.Breakdown.Verification.IssuerVerified ? "registry" : null
},
GeneratedAt = DateTimeOffset.UtcNow
GeneratedAt = _timeProvider.GetUtcNow()
};
}
@@ -604,10 +607,11 @@ public sealed class TrustScorecardApiService : ITrustScorecardApiService
};
}
var now = _timeProvider.GetUtcNow();
var history = await _historyStore.GetHistoryAsync(
sourceId,
DateTimeOffset.UtcNow.AddDays(-days),
DateTimeOffset.UtcNow,
now.AddDays(-days),
now,
cancellationToken);
if (history.Count == 0)
@@ -622,7 +626,7 @@ public sealed class TrustScorecardApiService : ITrustScorecardApiService
var current = history.LastOrDefault()?.CompositeScore ?? 0.0;
var thirtyDaysAgo = history
.Where(h => h.Timestamp >= DateTimeOffset.UtcNow.AddDays(-30))
.Where(h => h.Timestamp >= now.AddDays(-30))
.FirstOrDefault()?.CompositeScore ?? current;
var ninetyDaysAgo = history.FirstOrDefault()?.CompositeScore ?? current;

View File

@@ -138,14 +138,16 @@ public sealed class InMemoryConsensusRationaleCache : IConsensusRationaleCache
private readonly Dictionary<string, CacheEntry> _cache = new();
private readonly object _lock = new();
private readonly int _maxEntries;
private readonly TimeProvider _timeProvider;
private long _hitCount;
private long _missCount;
private DateTimeOffset? _lastCleared;
public InMemoryConsensusRationaleCache(int maxEntries = 10000)
public InMemoryConsensusRationaleCache(int maxEntries = 10000, TimeProvider? timeProvider = null)
{
_maxEntries = maxEntries;
_timeProvider = timeProvider ?? TimeProvider.System;
}
public Task<DetailedConsensusRationale?> GetAsync(
@@ -163,7 +165,7 @@ public sealed class InMemoryConsensusRationaleCache : IConsensusRationaleCache
return Task.FromResult<DetailedConsensusRationale?>(null);
}
entry.LastAccessed = DateTimeOffset.UtcNow;
entry.LastAccessed = _timeProvider.GetUtcNow();
Interlocked.Increment(ref _hitCount);
return Task.FromResult<DetailedConsensusRationale?>(entry.Rationale);
}
@@ -187,12 +189,13 @@ public sealed class InMemoryConsensusRationaleCache : IConsensusRationaleCache
EvictOldestEntry();
}
var now = _timeProvider.GetUtcNow();
_cache[cacheKey] = new CacheEntry
{
Rationale = rationale,
Options = options ?? new CacheOptions(),
Created = DateTimeOffset.UtcNow,
LastAccessed = DateTimeOffset.UtcNow
Created = now,
LastAccessed = now
};
return Task.CompletedTask;
@@ -254,7 +257,7 @@ public sealed class InMemoryConsensusRationaleCache : IConsensusRationaleCache
lock (_lock)
{
_cache.Clear();
_lastCleared = DateTimeOffset.UtcNow;
_lastCleared = _timeProvider.GetUtcNow();
return Task.CompletedTask;
}
}
@@ -277,9 +280,9 @@ public sealed class InMemoryConsensusRationaleCache : IConsensusRationaleCache
}
}
private static bool IsExpired(CacheEntry entry)
private bool IsExpired(CacheEntry entry)
{
var now = DateTimeOffset.UtcNow;
var now = _timeProvider.GetUtcNow();
if (entry.Options.AbsoluteExpiration.HasValue &&
now >= entry.Options.AbsoluteExpiration.Value)

View File

@@ -13,10 +13,14 @@ namespace StellaOps.VexLens.Consensus;
public sealed class VexConsensusEngine : IVexConsensusEngine
{
private ConsensusConfiguration _configuration;
private readonly TimeProvider _timeProvider;
public VexConsensusEngine(ConsensusConfiguration? configuration = null)
public VexConsensusEngine(
ConsensusConfiguration? configuration = null,
TimeProvider? timeProvider = null)
{
_configuration = configuration ?? CreateDefaultConfiguration();
_timeProvider = timeProvider ?? TimeProvider.System;
}
public Task<VexConsensusResult> ComputeConsensusAsync(
@@ -559,7 +563,7 @@ public sealed class VexConsensusEngine : IVexConsensusEngine
stmt.Statement.Status,
stmt.Statement.Justification,
weight,
GetStatementTimestamp(stmt.Statement),
GetStatementTimestamp(stmt.Statement, _timeProvider),
HasSignature(stmt.Weight));
}
@@ -574,7 +578,7 @@ public sealed class VexConsensusEngine : IVexConsensusEngine
stmt.Statement.Status,
stmt.Statement.Justification,
weight,
GetStatementTimestamp(stmt.Statement),
GetStatementTimestamp(stmt.Statement, _timeProvider),
HasSignature(stmt.Weight),
reason);
}
@@ -704,7 +708,7 @@ public sealed class VexConsensusEngine : IVexConsensusEngine
stmt.Statement.Status,
stmt.Statement.Justification,
weight,
GetStatementTimestamp(stmt.Statement),
GetStatementTimestamp(stmt.Statement, _timeProvider),
HasSignature(stmt.Weight));
}
@@ -719,7 +723,7 @@ public sealed class VexConsensusEngine : IVexConsensusEngine
stmt.Statement.Status,
stmt.Statement.Justification,
weight,
GetStatementTimestamp(stmt.Statement),
GetStatementTimestamp(stmt.Statement, _timeProvider),
HasSignature(stmt.Weight),
reason);
}
@@ -1278,10 +1282,10 @@ public sealed class VexConsensusEngine : IVexConsensusEngine
(decimal)breakdown.StatusSpecificityWeight));
}
private static DateTimeOffset GetStatementTimestamp(NormalizedStatement statement)
private static DateTimeOffset GetStatementTimestamp(NormalizedStatement statement, TimeProvider timeProvider)
{
// Use LastSeen if available, otherwise FirstSeen, otherwise current time
return statement.LastSeen ?? statement.FirstSeen ?? DateTimeOffset.UtcNow;
return statement.LastSeen ?? statement.FirstSeen ?? timeProvider.GetUtcNow();
}
private static bool HasSignature(Trust.TrustWeightResult weight)

View File

@@ -1,4 +1,5 @@
using System.Text.Json;
using StellaOps.Determinism;
using StellaOps.VexLens.Consensus;
using StellaOps.VexLens.Models;
using StellaOps.VexLens.Storage;
@@ -273,12 +274,19 @@ public enum ExportFormat
public sealed class ConsensusExportService : IConsensusExportService
{
private readonly IConsensusProjectionStore _projectionStore;
private readonly TimeProvider _timeProvider;
private readonly IGuidProvider _guidProvider;
private const string SnapshotVersion = "1.0.0";
public ConsensusExportService(IConsensusProjectionStore projectionStore)
public ConsensusExportService(
IConsensusProjectionStore projectionStore,
TimeProvider? timeProvider = null,
IGuidProvider? guidProvider = null)
{
_projectionStore = projectionStore;
_timeProvider = timeProvider ?? TimeProvider.System;
_guidProvider = guidProvider ?? new SystemGuidProvider();
}
public async Task<ConsensusSnapshot> CreateSnapshotAsync(
@@ -338,12 +346,12 @@ public sealed class ConsensusExportService : IConsensusExportService
.GroupBy(p => p.Status)
.ToDictionary(g => g.Key, g => g.Count());
var snapshotId = $"snap-{Guid.NewGuid():N}";
var snapshotId = $"snap-{_guidProvider.NewGuid():N}";
var contentHash = ComputeContentHash(projections);
return new ConsensusSnapshot(
SnapshotId: snapshotId,
CreatedAt: DateTimeOffset.UtcNow,
CreatedAt: _timeProvider.GetUtcNow(),
Version: SnapshotVersion,
TenantId: request.TenantId,
Projections: projections,
@@ -400,13 +408,13 @@ public sealed class ConsensusExportService : IConsensusExportService
// For a true incremental, we'd compare with the previous snapshot
// Here we just return new/updated since the timestamp
var snapshotId = $"snap-inc-{Guid.NewGuid():N}";
var snapshotId = $"snap-inc-{_guidProvider.NewGuid():N}";
var contentHash = ComputeContentHash(current.Projections);
return new IncrementalSnapshot(
SnapshotId: snapshotId,
PreviousSnapshotId: lastSnapshotId,
CreatedAt: DateTimeOffset.UtcNow,
CreatedAt: _timeProvider.GetUtcNow(),
Version: SnapshotVersion,
Added: current.Projections,
Removed: [], // Would need previous snapshot to determine removed

View File

@@ -2,6 +2,7 @@ using System.Diagnostics;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using StellaOps.Determinism;
using StellaOps.VexLens.Models;
namespace StellaOps.VexLens.Normalization;
@@ -12,6 +13,13 @@ namespace StellaOps.VexLens.Normalization;
/// </summary>
public sealed class CsafVexNormalizer : IVexNormalizer
{
private readonly IGuidProvider _guidProvider;
public CsafVexNormalizer(IGuidProvider? guidProvider = null)
{
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
}
public VexSourceFormat SourceFormat => VexSourceFormat.CsafVex;
public bool CanNormalize(string content)
@@ -77,7 +85,7 @@ public sealed class CsafVexNormalizer : IVexNormalizer
var documentId = ExtractDocumentId(documentElement);
if (string.IsNullOrWhiteSpace(documentId))
{
documentId = $"csaf:{Guid.NewGuid():N}";
documentId = $"csaf:{_guidProvider.NewGuid():N}";
warnings.Add(new NormalizationWarning(
"WARN_CSAF_001",
"Document tracking ID not found; generated a random ID",

View File

@@ -2,6 +2,7 @@ using System.Diagnostics;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using StellaOps.Determinism;
using StellaOps.VexLens.Models;
namespace StellaOps.VexLens.Normalization;
@@ -12,6 +13,13 @@ namespace StellaOps.VexLens.Normalization;
/// </summary>
public sealed class CycloneDxVexNormalizer : IVexNormalizer
{
private readonly IGuidProvider _guidProvider;
public CycloneDxVexNormalizer(IGuidProvider? guidProvider = null)
{
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
}
public VexSourceFormat SourceFormat => VexSourceFormat.CycloneDxVex;
public bool CanNormalize(string content)
@@ -65,7 +73,7 @@ public sealed class CycloneDxVexNormalizer : IVexNormalizer
var documentId = ExtractDocumentId(root);
if (string.IsNullOrWhiteSpace(documentId))
{
documentId = $"cyclonedx:{Guid.NewGuid():N}";
documentId = $"cyclonedx:{_guidProvider.NewGuid():N}";
warnings.Add(new NormalizationWarning(
"WARN_CDX_001",
"Serial number not found; generated a random ID",

View File

@@ -2,6 +2,7 @@ using System.Diagnostics;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using StellaOps.Determinism;
using StellaOps.VexLens.Models;
namespace StellaOps.VexLens.Normalization;
@@ -11,6 +12,13 @@ namespace StellaOps.VexLens.Normalization;
/// </summary>
public sealed class OpenVexNormalizer : IVexNormalizer
{
private readonly IGuidProvider _guidProvider;
public OpenVexNormalizer(IGuidProvider? guidProvider = null)
{
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
}
public VexSourceFormat SourceFormat => VexSourceFormat.OpenVex;
public bool CanNormalize(string content)
@@ -58,7 +66,7 @@ public sealed class OpenVexNormalizer : IVexNormalizer
var documentId = ExtractDocumentId(root);
if (string.IsNullOrWhiteSpace(documentId))
{
documentId = $"openvex:{Guid.NewGuid():N}";
documentId = $"openvex:{_guidProvider.NewGuid():N}";
warnings.Add(new NormalizationWarning(
"WARN_OPENVEX_001",
"Document ID not found; generated a random ID",
@@ -207,7 +215,7 @@ public sealed class OpenVexNormalizer : IVexNormalizer
return null;
}
private static IReadOnlyList<NormalizedStatement> ExtractStatements(
private IReadOnlyList<NormalizedStatement> ExtractStatements(
JsonElement root,
List<NormalizationWarning> warnings,
ref int skipped)
@@ -227,7 +235,7 @@ public sealed class OpenVexNormalizer : IVexNormalizer
foreach (var stmt in statementsArray.EnumerateArray())
{
var statement = ExtractStatement(stmt, index, warnings, ref skipped);
var statement = ExtractStatement(stmt, index, warnings, ref skipped, _guidProvider);
if (statement != null)
{
statements.Add(statement);
@@ -243,7 +251,8 @@ public sealed class OpenVexNormalizer : IVexNormalizer
JsonElement stmt,
int index,
List<NormalizationWarning> warnings,
ref int skipped)
ref int skipped,
IGuidProvider? guidProvider = null)
{
// Extract vulnerability
string? vulnerabilityId = null;
@@ -298,7 +307,7 @@ public sealed class OpenVexNormalizer : IVexNormalizer
{
foreach (var prod in productsArray.EnumerateArray())
{
var product = ExtractProduct(prod);
var product = ExtractProduct(prod, guidProvider);
if (product != null)
{
products.Add(product);
@@ -378,7 +387,7 @@ public sealed class OpenVexNormalizer : IVexNormalizer
LastSeen: timestamp);
}
private static NormalizedProduct? ExtractProduct(JsonElement prod)
private static NormalizedProduct? ExtractProduct(JsonElement prod, IGuidProvider? guidProvider = null)
{
string? key = null;
string? name = null;
@@ -423,8 +432,9 @@ public sealed class OpenVexNormalizer : IVexNormalizer
return null;
}
var fallbackGuid = guidProvider?.NewGuid() ?? Guid.NewGuid();
return new NormalizedProduct(
Key: key ?? purl ?? cpe ?? $"unknown-{Guid.NewGuid():N}",
Key: key ?? purl ?? cpe ?? $"unknown-{fallbackGuid:N}",
Name: name,
Version: version,
Purl: purl,

View File

@@ -160,6 +160,7 @@ public sealed class ConsensusJobService : IConsensusJobService
private readonly IVexConsensusEngine _consensusEngine;
private readonly IConsensusProjectionStore _projectionStore;
private readonly IConsensusExportService _exportService;
private readonly TimeProvider _timeProvider;
private const string SchemaVersion = "1.0.0";
@@ -172,11 +173,13 @@ public sealed class ConsensusJobService : IConsensusJobService
public ConsensusJobService(
IVexConsensusEngine consensusEngine,
IConsensusProjectionStore projectionStore,
IConsensusExportService exportService)
IConsensusExportService exportService,
TimeProvider? timeProvider = null)
{
_consensusEngine = consensusEngine;
_projectionStore = projectionStore;
_exportService = exportService;
_timeProvider = timeProvider ?? TimeProvider.System;
}
public ConsensusJobRequest CreateComputeJob(
@@ -299,7 +302,7 @@ public sealed class ConsensusJobService : IConsensusJobService
JobType: ConsensusJobTypes.SnapshotCreate,
TenantId: request.TenantId,
Priority: ConsensusJobTypes.GetDefaultPriority(ConsensusJobTypes.SnapshotCreate),
IdempotencyKey: $"snapshot:{requestHash}:{DateTimeOffset.UtcNow:yyyyMMddHHmm}",
IdempotencyKey: $"snapshot:{requestHash}:{_timeProvider.GetUtcNow():yyyyMMddHHmm}",
Payload: JsonSerializer.Serialize(payload, JsonOptions));
}
@@ -307,7 +310,7 @@ public sealed class ConsensusJobService : IConsensusJobService
ConsensusJobRequest request,
CancellationToken cancellationToken = default)
{
var startTime = DateTimeOffset.UtcNow;
var startTime = _timeProvider.GetUtcNow();
try
{
@@ -350,7 +353,7 @@ public sealed class ConsensusJobService : IConsensusJobService
ConsensusJobRequest request,
CancellationToken cancellationToken)
{
var startTime = DateTimeOffset.UtcNow;
var startTime = _timeProvider.GetUtcNow();
var payload = JsonSerializer.Deserialize<ComputePayload>(request.Payload, JsonOptions)
?? throw new InvalidOperationException("Invalid compute payload");
@@ -363,7 +366,7 @@ public sealed class ConsensusJobService : IConsensusJobService
JobType: request.JobType,
ItemsProcessed: 1,
ItemsFailed: 0,
Duration: DateTimeOffset.UtcNow - startTime,
Duration: _timeProvider.GetUtcNow() - startTime,
ResultPayload: JsonSerializer.Serialize(new
{
vulnerabilityId = payload.VulnerabilityId,
@@ -377,7 +380,7 @@ public sealed class ConsensusJobService : IConsensusJobService
ConsensusJobRequest request,
CancellationToken cancellationToken)
{
var startTime = DateTimeOffset.UtcNow;
var startTime = _timeProvider.GetUtcNow();
var payload = JsonSerializer.Deserialize<BatchComputePayload>(request.Payload, JsonOptions)
?? throw new InvalidOperationException("Invalid batch compute payload");
@@ -389,7 +392,7 @@ public sealed class ConsensusJobService : IConsensusJobService
JobType: request.JobType,
ItemsProcessed: itemCount,
ItemsFailed: 0,
Duration: DateTimeOffset.UtcNow - startTime,
Duration: _timeProvider.GetUtcNow() - startTime,
ResultPayload: JsonSerializer.Serialize(new { processedCount = itemCount }, JsonOptions),
ErrorMessage: null);
}
@@ -398,7 +401,7 @@ public sealed class ConsensusJobService : IConsensusJobService
ConsensusJobRequest request,
CancellationToken cancellationToken)
{
var startTime = DateTimeOffset.UtcNow;
var startTime = _timeProvider.GetUtcNow();
// Create snapshot using export service
var snapshotRequest = ConsensusExportExtensions.FullExportRequest(request.TenantId);
@@ -409,7 +412,7 @@ public sealed class ConsensusJobService : IConsensusJobService
JobType: request.JobType,
ItemsProcessed: snapshot.Projections.Count,
ItemsFailed: 0,
Duration: DateTimeOffset.UtcNow - startTime,
Duration: _timeProvider.GetUtcNow() - startTime,
ResultPayload: JsonSerializer.Serialize(new
{
snapshotId = snapshot.SnapshotId,
@@ -419,14 +422,14 @@ public sealed class ConsensusJobService : IConsensusJobService
ErrorMessage: null);
}
private static ConsensusJobResult CreateFailedResult(string jobType, DateTimeOffset startTime, string error)
private ConsensusJobResult CreateFailedResult(string jobType, DateTimeOffset startTime, string error)
{
return new ConsensusJobResult(
Success: false,
JobType: jobType,
ItemsProcessed: 0,
ItemsFailed: 1,
Duration: DateTimeOffset.UtcNow - startTime,
Duration: _timeProvider.GetUtcNow() - startTime,
ResultPayload: null,
ErrorMessage: error);
}

View File

@@ -1,4 +1,5 @@
using System.Text.Json;
using StellaOps.Determinism;
using StellaOps.VexLens.Consensus;
using StellaOps.VexLens.Models;
using StellaOps.VexLens.Storage;
@@ -14,6 +15,8 @@ public sealed class OrchestratorLedgerEventEmitter : IConsensusEventEmitter
{
private readonly IOrchestratorLedgerClient? _ledgerClient;
private readonly OrchestratorEventOptions _options;
private readonly TimeProvider _timeProvider;
private readonly IGuidProvider _guidProvider;
private static readonly JsonSerializerOptions JsonOptions = new()
{
@@ -23,10 +26,14 @@ public sealed class OrchestratorLedgerEventEmitter : IConsensusEventEmitter
public OrchestratorLedgerEventEmitter(
IOrchestratorLedgerClient? ledgerClient = null,
OrchestratorEventOptions? options = null)
OrchestratorEventOptions? options = null,
TimeProvider? timeProvider = null,
IGuidProvider? guidProvider = null)
{
_ledgerClient = ledgerClient;
_options = options ?? OrchestratorEventOptions.Default;
_timeProvider = timeProvider ?? TimeProvider.System;
_guidProvider = guidProvider ?? new SystemGuidProvider();
}
public async Task EmitConsensusComputedAsync(
@@ -144,11 +151,11 @@ public sealed class OrchestratorLedgerEventEmitter : IConsensusEventEmitter
if (_ledgerClient == null) return;
var alertEvent = new LedgerEvent(
EventId: $"alert-{Guid.NewGuid():N}",
EventId: $"alert-{_guidProvider.NewGuid():N}",
EventType: ConsensusEventTypes.Alert,
TenantId: @event.TenantId,
CorrelationId: @event.EventId,
OccurredAt: DateTimeOffset.UtcNow,
OccurredAt: _timeProvider.GetUtcNow(),
IdempotencyKey: $"alert-status-{@event.ProjectionId}-{@event.NewStatus}",
Actor: new LedgerActor("system", "vexlens", "alert-engine"),
Payload: JsonSerializer.Serialize(new
@@ -174,11 +181,11 @@ public sealed class OrchestratorLedgerEventEmitter : IConsensusEventEmitter
if (_ledgerClient == null) return;
var alertEvent = new LedgerEvent(
EventId: $"alert-{Guid.NewGuid():N}",
EventId: $"alert-{_guidProvider.NewGuid():N}",
EventType: ConsensusEventTypes.Alert,
TenantId: @event.TenantId,
CorrelationId: @event.EventId,
OccurredAt: DateTimeOffset.UtcNow,
OccurredAt: _timeProvider.GetUtcNow(),
IdempotencyKey: $"alert-conflict-{@event.ProjectionId}",
Actor: new LedgerActor("system", "vexlens", "alert-engine"),
Payload: JsonSerializer.Serialize(new

View File

@@ -1,6 +1,7 @@
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
using System.Collections.Immutable;
using StellaOps.Determinism;
using StellaOps.VexLens.Consensus;
using StellaOps.VexLens.Models;
@@ -13,6 +14,7 @@ namespace StellaOps.VexLens.Proof;
public sealed class VexProofBuilder
{
private readonly TimeProvider _timeProvider;
private readonly IGuidProvider _guidProvider;
private readonly List<VexProofStatement> _statements = [];
private readonly List<VexProofMergeStep> _mergeSteps = [];
private readonly List<VexProofConflict> _conflicts = [];
@@ -48,11 +50,12 @@ public sealed class VexProofBuilder
private decimal _conditionCoverage = 1.0m;
/// <summary>
/// Creates a new VexProofBuilder with the specified time provider.
/// Creates a new VexProofBuilder with the specified time provider and GUID provider.
/// </summary>
public VexProofBuilder(TimeProvider timeProvider)
public VexProofBuilder(TimeProvider timeProvider, IGuidProvider? guidProvider = null)
{
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
}
/// <summary>
@@ -533,10 +536,10 @@ public sealed class VexProofBuilder
_ => ConfidenceTier.VeryLow
};
private static string GenerateProofId(DateTimeOffset timestamp)
private string GenerateProofId(DateTimeOffset timestamp)
{
var timePart = timestamp.ToString("yyyy-MM-ddTHH:mm:ssZ", System.Globalization.CultureInfo.InvariantCulture);
var randomPart = Guid.NewGuid().ToString("N")[..8];
var randomPart = _guidProvider.NewGuid().ToString("N")[..8];
return $"proof-{timePart}-{randomPart}";
}
}

View File

@@ -30,6 +30,8 @@
<!-- NG-001: Noise-gating dependencies -->
<ProjectReference Include="..\..\__Libraries\StellaOps.ReachGraph\StellaOps.ReachGraph.csproj" />
<ProjectReference Include="..\..\Policy\StellaOps.Policy.Engine\StellaOps.Policy.Engine.csproj" />
<!-- DET-015: Determinism abstractions for TimeProvider and IGuidProvider -->
<ProjectReference Include="..\..\__Libraries\StellaOps.Determinism.Abstractions\StellaOps.Determinism.Abstractions.csproj" />
</ItemGroup>
<!-- Exclude legacy folders with external dependencies -->

View File

@@ -1,4 +1,5 @@
using System.Collections.Concurrent;
using StellaOps.Determinism;
using StellaOps.VexLens.Consensus;
using StellaOps.VexLens.Models;
using StellaOps.VexLens.Services;
@@ -16,13 +17,19 @@ public sealed class InMemoryConsensusProjectionStore : IConsensusProjectionStore
private readonly IConsensusEventEmitter? _eventEmitter;
// LIN-BE-009: Delta service for computing VEX deltas on status change
private readonly IVexDeltaComputeService? _deltaComputeService;
private readonly TimeProvider _timeProvider;
private readonly IGuidProvider _guidProvider;
public InMemoryConsensusProjectionStore(
IConsensusEventEmitter? eventEmitter = null,
IVexDeltaComputeService? deltaComputeService = null)
IVexDeltaComputeService? deltaComputeService = null,
TimeProvider? timeProvider = null,
IGuidProvider? guidProvider = null)
{
_eventEmitter = eventEmitter;
_deltaComputeService = deltaComputeService;
_timeProvider = timeProvider ?? TimeProvider.System;
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
}
public async Task<ConsensusProjection> StoreAsync(
@@ -31,7 +38,7 @@ public sealed class InMemoryConsensusProjectionStore : IConsensusProjectionStore
CancellationToken cancellationToken = default)
{
var key = GetKey(result.VulnerabilityId, result.ProductKey, options.TenantId);
var now = DateTimeOffset.UtcNow;
var now = _timeProvider.GetUtcNow();
// Get previous projection for history tracking
ConsensusProjection? previous = null;
@@ -52,7 +59,7 @@ public sealed class InMemoryConsensusProjectionStore : IConsensusProjectionStore
}
var projection = new ConsensusProjection(
ProjectionId: $"proj-{Guid.NewGuid():N}",
ProjectionId: $"proj-{_guidProvider.NewGuid():N}",
VulnerabilityId: result.VulnerabilityId,
ProductKey: result.ProductKey,
TenantId: options.TenantId,
@@ -283,12 +290,12 @@ public sealed class InMemoryConsensusProjectionStore : IConsensusProjectionStore
{
if (_eventEmitter == null) return;
var now = DateTimeOffset.UtcNow;
var now = _timeProvider.GetUtcNow();
// Always emit computed event
await _eventEmitter.EmitConsensusComputedAsync(
new ConsensusComputedEvent(
EventId: $"evt-{Guid.NewGuid():N}",
EventId: $"evt-{_guidProvider.NewGuid():N}",
ProjectionId: projection.ProjectionId,
VulnerabilityId: projection.VulnerabilityId,
ProductKey: projection.ProductKey,
@@ -307,7 +314,7 @@ public sealed class InMemoryConsensusProjectionStore : IConsensusProjectionStore
{
await _eventEmitter.EmitStatusChangedAsync(
new ConsensusStatusChangedEvent(
EventId: $"evt-{Guid.NewGuid():N}",
EventId: $"evt-{_guidProvider.NewGuid():N}",
ProjectionId: projection.ProjectionId,
VulnerabilityId: projection.VulnerabilityId,
ProductKey: projection.ProductKey,
@@ -325,7 +332,7 @@ public sealed class InMemoryConsensusProjectionStore : IConsensusProjectionStore
await _deltaComputeService.ComputeAndStoreAsync(
new VexStatusChangeContext
{
ProjectionId = Guid.TryParse(projection.ProjectionId, out var pid) ? pid : Guid.NewGuid(),
ProjectionId = Guid.TryParse(projection.ProjectionId, out var pid) ? pid : _guidProvider.NewGuid(),
VulnerabilityId = projection.VulnerabilityId,
ProductKey = projection.ProductKey,
ArtifactDigest = projection.ProductKey, // Use ProductKey as artifact identifier
@@ -355,7 +362,7 @@ public sealed class InMemoryConsensusProjectionStore : IConsensusProjectionStore
await _eventEmitter.EmitConflictDetectedAsync(
new ConsensusConflictDetectedEvent(
EventId: $"evt-{Guid.NewGuid():N}",
EventId: $"evt-{_guidProvider.NewGuid():N}",
ProjectionId: projection.ProjectionId,
VulnerabilityId: projection.VulnerabilityId,
ProductKey: projection.ProductKey,

View File

@@ -4,6 +4,7 @@
using System.Diagnostics;
using Microsoft.Extensions.Logging;
using Npgsql;
using StellaOps.Determinism;
using StellaOps.VexLens.Consensus;
using StellaOps.VexLens.Models;
using StellaOps.VexLens.Options;
@@ -28,19 +29,22 @@ public sealed class PostgresConsensusProjectionStoreProxy : IConsensusProjection
private readonly ILogger<PostgresConsensusProjectionStoreProxy> _logger;
private readonly VexLensStorageOptions _options;
private readonly TimeProvider _timeProvider;
private readonly IGuidProvider _guidProvider;
public PostgresConsensusProjectionStoreProxy(
NpgsqlDataSource dataSource,
ILogger<PostgresConsensusProjectionStoreProxy> logger,
IConsensusEventEmitter? eventEmitter = null,
VexLensStorageOptions? options = null,
TimeProvider? timeProvider = null)
TimeProvider? timeProvider = null,
IGuidProvider? guidProvider = null)
{
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_eventEmitter = eventEmitter;
_options = options ?? new VexLensStorageOptions();
_timeProvider = timeProvider ?? TimeProvider.System;
_guidProvider = guidProvider ?? new SystemGuidProvider();
}
private const string Schema = "vexlens";
@@ -108,7 +112,7 @@ public sealed class PostgresConsensusProjectionStoreProxy : IConsensusProjection
activity?.SetTag("vulnerabilityId", result.VulnerabilityId);
activity?.SetTag("productKey", result.ProductKey);
var projectionId = Guid.NewGuid();
var projectionId = _guidProvider.NewGuid();
var now = _timeProvider.GetUtcNow();
// Check for previous projection to track history
@@ -517,7 +521,7 @@ public sealed class PostgresConsensusProjectionStoreProxy : IConsensusProjection
var now = _timeProvider.GetUtcNow();
var computedEvent = new ConsensusComputedEvent(
EventId: $"evt-{Guid.NewGuid():N}",
EventId: $"evt-{_guidProvider.NewGuid():N}",
ProjectionId: projection.ProjectionId,
VulnerabilityId: projection.VulnerabilityId,
ProductKey: projection.ProductKey,
@@ -535,7 +539,7 @@ public sealed class PostgresConsensusProjectionStoreProxy : IConsensusProjection
if (projection.StatusChanged && previous is not null)
{
var changedEvent = new ConsensusStatusChangedEvent(
EventId: $"evt-{Guid.NewGuid():N}",
EventId: $"evt-{_guidProvider.NewGuid():N}",
ProjectionId: projection.ProjectionId,
VulnerabilityId: projection.VulnerabilityId,
ProductKey: projection.ProductKey,

View File

@@ -65,9 +65,9 @@ public sealed record SourceTrustScoreRequest
public required SourceVerificationSummary VerificationSummary { get; init; }
/// <summary>
/// Time at which to evaluate the score.
/// Time at which to evaluate the score. Required for determinism.
/// </summary>
public DateTimeOffset EvaluationTime { get; init; } = DateTimeOffset.UtcNow;
public required DateTimeOffset EvaluationTime { get; init; }
/// <summary>
/// Previous score for trend calculation.

View File

@@ -9,16 +9,18 @@ public sealed class InMemorySourceTrustScoreCache : ISourceTrustScoreCache
{
private readonly ConcurrentDictionary<string, CacheEntry> _cache = new();
private readonly Timer _cleanupTimer;
private readonly TimeProvider _timeProvider;
public InMemorySourceTrustScoreCache()
public InMemorySourceTrustScoreCache(TimeProvider? timeProvider = null)
{
_timeProvider = timeProvider ?? TimeProvider.System;
// Clean up expired entries every 5 minutes
_cleanupTimer = new Timer(CleanupExpiredEntries, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
}
public Task<VexSourceTrustScore?> GetAsync(string sourceId, CancellationToken cancellationToken = default)
{
if (_cache.TryGetValue(sourceId, out var entry) && entry.ExpiresAt > DateTimeOffset.UtcNow)
if (_cache.TryGetValue(sourceId, out var entry) && entry.ExpiresAt > _timeProvider.GetUtcNow())
{
return Task.FromResult<VexSourceTrustScore?>(entry.Score);
}
@@ -28,7 +30,7 @@ public sealed class InMemorySourceTrustScoreCache : ISourceTrustScoreCache
public Task SetAsync(string sourceId, VexSourceTrustScore score, TimeSpan duration, CancellationToken cancellationToken = default)
{
var entry = new CacheEntry(score, DateTimeOffset.UtcNow + duration);
var entry = new CacheEntry(score, _timeProvider.GetUtcNow() + duration);
_cache[sourceId] = entry;
return Task.CompletedTask;
}
@@ -41,7 +43,7 @@ public sealed class InMemorySourceTrustScoreCache : ISourceTrustScoreCache
private void CleanupExpiredEntries(object? state)
{
var now = DateTimeOffset.UtcNow;
var now = _timeProvider.GetUtcNow();
var expiredKeys = _cache
.Where(kvp => kvp.Value.ExpiresAt <= now)
.Select(kvp => kvp.Key)

View File

@@ -11,13 +11,16 @@ public sealed class ProvenanceChainValidator : IProvenanceChainValidator
{
private readonly ILogger<ProvenanceChainValidator> _logger;
private readonly IIssuerDirectory _issuerDirectory;
private readonly TimeProvider _timeProvider;
public ProvenanceChainValidator(
ILogger<ProvenanceChainValidator> logger,
IIssuerDirectory issuerDirectory)
IIssuerDirectory issuerDirectory,
TimeProvider? timeProvider = null)
{
_logger = logger;
_issuerDirectory = issuerDirectory;
_timeProvider = timeProvider ?? TimeProvider.System;
}
public async Task<ProvenanceValidationResult> ValidateAsync(
@@ -44,7 +47,7 @@ public sealed class ProvenanceChainValidator : IProvenanceChainValidator
// Validate chain age
if (options.MaxChainAge.HasValue)
{
var chainAge = DateTimeOffset.UtcNow - chain.Origin.Timestamp;
var chainAge = _timeProvider.GetUtcNow() - chain.Origin.Timestamp;
if (chainAge > options.MaxChainAge.Value)
{
issues.Add(new ProvenanceIssue

View File

@@ -11,6 +11,12 @@ public sealed class InMemoryIssuerDirectory : IIssuerDirectory
{
private readonly ConcurrentDictionary<string, IssuerRecord> _issuers = new(StringComparer.OrdinalIgnoreCase);
private readonly ConcurrentDictionary<string, string> _fingerprintToIssuer = new(StringComparer.OrdinalIgnoreCase);
private readonly TimeProvider _timeProvider;
public InMemoryIssuerDirectory(TimeProvider? timeProvider = null)
{
_timeProvider = timeProvider ?? TimeProvider.System;
}
public Task<IssuerRecord?> GetIssuerAsync(
string issuerId,
@@ -86,7 +92,7 @@ public sealed class InMemoryIssuerDirectory : IIssuerDirectory
IssuerRegistration registration,
CancellationToken cancellationToken = default)
{
var now = DateTimeOffset.UtcNow;
var now = _timeProvider.GetUtcNow();
var keyRecords = new List<KeyFingerprintRecord>();
if (registration.InitialKeys != null)
@@ -135,7 +141,7 @@ public sealed class InMemoryIssuerDirectory : IIssuerDirectory
return Task.FromResult(false);
}
var now = DateTimeOffset.UtcNow;
var now = _timeProvider.GetUtcNow();
var updated = current with
{
Status = IssuerStatus.Revoked,
@@ -165,7 +171,7 @@ public sealed class InMemoryIssuerDirectory : IIssuerDirectory
throw new InvalidOperationException($"Issuer '{issuerId}' not found");
}
var now = DateTimeOffset.UtcNow;
var now = _timeProvider.GetUtcNow();
var newKey = new KeyFingerprintRecord(
Fingerprint: keyRegistration.Fingerprint,
KeyType: keyRegistration.KeyType,
@@ -209,7 +215,7 @@ public sealed class InMemoryIssuerDirectory : IIssuerDirectory
return Task.FromResult(false);
}
var now = DateTimeOffset.UtcNow;
var now = _timeProvider.GetUtcNow();
var revokedKey = keyIndex.k with
{
Status = KeyFingerprintStatus.Revoked,
@@ -284,7 +290,7 @@ public sealed class InMemoryIssuerDirectory : IIssuerDirectory
keyStatus = KeyTrustStatus.Revoked;
warnings.Add($"Key was revoked: {key.RevocationReason}");
}
else if (key.ExpiresAt.HasValue && key.ExpiresAt.Value < DateTimeOffset.UtcNow)
else if (key.ExpiresAt.HasValue && key.ExpiresAt.Value < _timeProvider.GetUtcNow())
{
keyStatus = KeyTrustStatus.Expired;
warnings.Add($"Key expired on {key.ExpiresAt.Value:O}");