audit work, fixed StellaOps.sln warnings/errors, fixed tests, sprints work, new advisories

This commit is contained in:
master
2026-01-07 18:49:59 +02:00
parent 04ec098046
commit 608a7f85c0
866 changed files with 56323 additions and 6231 deletions

View File

@@ -25,6 +25,5 @@
<ItemGroup>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Moq" />
<PackageReference Include="xunit.runner.visualstudio" />
</ItemGroup>
</Project>

View File

@@ -2,6 +2,7 @@
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Options;
@@ -1867,7 +1868,7 @@ app.MapPatch("/api/v1/findings/{findingId}/state", async Task<Results<Ok<StateTr
var payload = new JsonObject { ["status"] = targetState, ["previous_status"] = previousStatus };
if (!string.IsNullOrWhiteSpace(request.Justification)) payload["justification"] = request.Justification;
if (!string.IsNullOrWhiteSpace(request.Notes)) payload["notes"] = request.Notes;
if (request.DueDate.HasValue) payload["due_date"] = request.DueDate.Value.ToString("O");
if (request.DueDate.HasValue) payload["due_date"] = request.DueDate.Value.ToString("O", CultureInfo.InvariantCulture);
if (request.Tags is { Count: > 0 })
{
var tagsArray = new JsonArray();

View File

@@ -1,3 +1,4 @@
using System.Globalization;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging;
@@ -49,8 +50,8 @@ public sealed class AttestationQueryService
["finding_id"] = request.FindingId,
["attestation_id"] = request.AttestationId,
["status"] = request.Status,
["since_recorded_at"] = request.SinceRecordedAt?.ToString("O"),
["until_recorded_at"] = request.UntilRecordedAt?.ToString("O"),
["since_recorded_at"] = request.SinceRecordedAt?.ToString("O", CultureInfo.InvariantCulture),
["until_recorded_at"] = request.UntilRecordedAt?.ToString("O", CultureInfo.InvariantCulture),
["limit"] = request.Limit.ToString()
};
@@ -119,7 +120,7 @@ public sealed class AttestationQueryService
FiltersHash = filtersHash,
Last = new AttestationPageKey
{
RecordedAt = key.RecordedAt.ToString("O"),
RecordedAt = key.RecordedAt.ToString("O", CultureInfo.InvariantCulture),
AttestationId = key.AttestationId
}
};

View File

@@ -1,3 +1,4 @@
using System.Globalization;
using System.Text.Json.Nodes;
using Microsoft.Extensions.Logging;
using Npgsql;
@@ -39,10 +40,10 @@ public sealed class ExportQueryService
["shape"] = request.Shape,
["since_sequence"] = request.SinceSequence?.ToString(),
["until_sequence"] = request.UntilSequence?.ToString(),
["since_observed_at"] = request.SinceObservedAt?.ToString("O"),
["until_observed_at"] = request.UntilObservedAt?.ToString("O"),
["since_observed_at"] = request.SinceObservedAt?.ToString("O", CultureInfo.InvariantCulture),
["until_observed_at"] = request.UntilObservedAt?.ToString("O", CultureInfo.InvariantCulture),
["status"] = request.Status,
["severity"] = request.Severity?.ToString()
["severity"] = request.Severity?.ToString(CultureInfo.InvariantCulture)
};
return ExportPaging.ComputeFiltersHash(filters);
@@ -55,8 +56,8 @@ public sealed class ExportQueryService
["shape"] = request.Shape,
["since_sequence"] = request.SinceSequence?.ToString(),
["until_sequence"] = request.UntilSequence?.ToString(),
["since_observed_at"] = request.SinceObservedAt?.ToString("O"),
["until_observed_at"] = request.UntilObservedAt?.ToString("O"),
["since_observed_at"] = request.SinceObservedAt?.ToString("O", CultureInfo.InvariantCulture),
["until_observed_at"] = request.UntilObservedAt?.ToString("O", CultureInfo.InvariantCulture),
["product_id"] = request.ProductId,
["advisory_id"] = request.AdvisoryId,
["status"] = request.Status,
@@ -73,8 +74,8 @@ public sealed class ExportQueryService
["shape"] = request.Shape,
["since_sequence"] = request.SinceSequence?.ToString(),
["until_sequence"] = request.UntilSequence?.ToString(),
["since_observed_at"] = request.SinceObservedAt?.ToString("O"),
["until_observed_at"] = request.UntilObservedAt?.ToString("O"),
["since_observed_at"] = request.SinceObservedAt?.ToString("O", CultureInfo.InvariantCulture),
["until_observed_at"] = request.UntilObservedAt?.ToString("O", CultureInfo.InvariantCulture),
["severity"] = request.Severity,
["source"] = request.Source,
["cwe_id"] = request.CweId,
@@ -94,8 +95,8 @@ public sealed class ExportQueryService
["shape"] = request.Shape,
["since_sequence"] = request.SinceSequence?.ToString(),
["until_sequence"] = request.UntilSequence?.ToString(),
["since_observed_at"] = request.SinceObservedAt?.ToString("O"),
["until_observed_at"] = request.UntilObservedAt?.ToString("O"),
["since_observed_at"] = request.SinceObservedAt?.ToString("O", CultureInfo.InvariantCulture),
["until_observed_at"] = request.UntilObservedAt?.ToString("O", CultureInfo.InvariantCulture),
["subject_digest"] = request.SubjectDigest,
["sbom_format"] = request.SbomFormat,
["component_purl"] = request.ComponentPurl,

View File

@@ -1,4 +1,5 @@
using System.Diagnostics;
using System.Globalization;
using Microsoft.Extensions.Logging;
using StellaOps.Findings.Ledger.Domain;
using StellaOps.Findings.Ledger.Infrastructure.Exports;
@@ -152,7 +153,7 @@ internal static class LedgerTimeline
newFindings,
resolvedFindings,
criticalDelta,
timeAnchor.ToString("O"),
timeAnchor.ToString("O", CultureInfo.InvariantCulture),
sealedMode);
}
@@ -300,7 +301,7 @@ internal static class LedgerTimeline
snapshot.ActivationId ?? string.Empty,
snapshot.Actor ?? string.Empty,
snapshot.Reason ?? string.Empty,
snapshot.ExpiresAt?.ToString("O") ?? string.Empty,
snapshot.ExpiresAt?.ToString("O", CultureInfo.InvariantCulture) ?? string.Empty,
snapshot.RetentionExtensionDays,
wasReactivation);
}
@@ -321,8 +322,8 @@ internal static class LedgerTimeline
sample.EventType,
sample.PolicyVersion,
sample.LagSeconds,
sample.RecordedAt.ToString("O"),
sample.ObservedAt.ToString("O"));
sample.RecordedAt.ToString("O", CultureInfo.InvariantCulture),
sample.ObservedAt.ToString("O", CultureInfo.InvariantCulture));
}
public static void EmitIncidentConflictSnapshot(ILogger logger, ConflictSnapshot snapshot)
@@ -345,7 +346,7 @@ internal static class LedgerTimeline
snapshot.ExpectedSequence,
snapshot.ActorId ?? string.Empty,
snapshot.ActorType ?? string.Empty,
snapshot.ObservedAt.ToString("O"));
snapshot.ObservedAt.ToString("O", CultureInfo.InvariantCulture));
}
public static void EmitIncidentReplayTrace(ILogger logger, ReplayTraceSample sample)
@@ -366,6 +367,6 @@ internal static class LedgerTimeline
sample.HasMore,
sample.ChainFilterCount,
sample.EventTypeFilterCount,
sample.ObservedAt.ToString("O"));
sample.ObservedAt.ToString("O", CultureInfo.InvariantCulture));
}
}

View File

@@ -0,0 +1,187 @@
// -----------------------------------------------------------------------------
// IObservationRepository.cs
// Sprint: SPRINT_20260106_001_004_BE_determinization_integration
// Task: DBI-012 - Create IObservationRepository in Findings
// Description: Repository interface for finding observations
// -----------------------------------------------------------------------------
namespace StellaOps.Findings.Ledger.Observations;
/// <summary>
/// Represents an observation in the findings ledger.
/// </summary>
public sealed record Observation
{
/// <summary>Unique observation ID.</summary>
public required string Id { get; init; }
/// <summary>The CVE ID.</summary>
public required string CveId { get; init; }
/// <summary>The product (purl or cpe).</summary>
public required string Product { get; init; }
/// <summary>Tenant ID.</summary>
public required string TenantId { get; init; }
/// <summary>Finding ID (if linked to a finding).</summary>
public string? FindingId { get; init; }
/// <summary>Current state.</summary>
public required ObservationState State { get; init; }
/// <summary>Previous state (for transitions).</summary>
public ObservationState? PreviousState { get; init; }
/// <summary>Reason for the current state.</summary>
public string? Reason { get; init; }
/// <summary>User who made the observation.</summary>
public string? UserId { get; init; }
/// <summary>Evidence bundle reference.</summary>
public string? EvidenceRef { get; init; }
/// <summary>Signal snapshot at time of observation.</summary>
public SignalSnapshotSummary? Signals { get; init; }
/// <summary>When the observation was created.</summary>
public required DateTimeOffset CreatedAt { get; init; }
/// <summary>Expiration time (if temporary).</summary>
public DateTimeOffset? ExpiresAt { get; init; }
}
/// <summary>
/// Observation state.
/// </summary>
public enum ObservationState
{
/// <summary>Under review.</summary>
UnderReview,
/// <summary>Confirmed affected.</summary>
Affected,
/// <summary>Confirmed not affected.</summary>
NotAffected,
/// <summary>Fixed.</summary>
Fixed,
/// <summary>Mitigated.</summary>
Mitigated,
/// <summary>Risk accepted.</summary>
Accepted,
/// <summary>False positive.</summary>
FalsePositive,
/// <summary>Deferred.</summary>
Deferred
}
/// <summary>
/// Summary of signals at time of observation.
/// </summary>
public sealed record SignalSnapshotSummary
{
/// <summary>EPSS score.</summary>
public double? EpssScore { get; init; }
/// <summary>EPSS percentile.</summary>
public double? EpssPercentile { get; init; }
/// <summary>Is in KEV.</summary>
public bool? IsInKev { get; init; }
/// <summary>VEX status.</summary>
public string? VexStatus { get; init; }
/// <summary>Reachability status.</summary>
public string? ReachabilityStatus { get; init; }
}
/// <summary>
/// Repository for observations in the findings ledger.
/// </summary>
public interface IObservationRepository
{
/// <summary>
/// Creates an observation.
/// </summary>
Task<Observation> CreateAsync(Observation observation, CancellationToken ct = default);
/// <summary>
/// Gets an observation by ID.
/// </summary>
Task<Observation?> GetByIdAsync(string id, CancellationToken ct = default);
/// <summary>
/// Gets observations for a CVE.
/// </summary>
Task<IReadOnlyList<Observation>> GetByCveAsync(
string cveId,
string tenantId,
CancellationToken ct = default);
/// <summary>
/// Gets observations for a product.
/// </summary>
Task<IReadOnlyList<Observation>> GetByProductAsync(
string product,
string tenantId,
CancellationToken ct = default);
/// <summary>
/// Gets observations for a finding.
/// </summary>
Task<IReadOnlyList<Observation>> GetByFindingAsync(
string findingId,
CancellationToken ct = default);
/// <summary>
/// Gets the latest observation for a CVE/product pair.
/// </summary>
Task<Observation?> GetLatestAsync(
string cveId,
string product,
string tenantId,
CancellationToken ct = default);
/// <summary>
/// Gets observation history for a CVE/product pair.
/// </summary>
Task<IReadOnlyList<Observation>> GetHistoryAsync(
string cveId,
string product,
string tenantId,
int limit = 100,
CancellationToken ct = default);
/// <summary>
/// Gets observations by state.
/// </summary>
Task<IReadOnlyList<Observation>> GetByStateAsync(
ObservationState state,
string tenantId,
int limit = 100,
int offset = 0,
CancellationToken ct = default);
/// <summary>
/// Gets expiring observations.
/// </summary>
Task<IReadOnlyList<Observation>> GetExpiringAsync(
DateTimeOffset before,
string tenantId,
CancellationToken ct = default);
/// <summary>
/// Counts observations by state.
/// </summary>
Task<IDictionary<ObservationState, int>> CountByStateAsync(
string tenantId,
CancellationToken ct = default);
}

View File

@@ -0,0 +1,342 @@
// -----------------------------------------------------------------------------
// PostgresObservationRepository.cs
// Sprint: SPRINT_20260106_001_004_BE_determinization_integration
// Task: DBI-013 - Implement PostgresObservationRepository
// Description: PostgreSQL implementation of observation repository
// -----------------------------------------------------------------------------
using System.Globalization;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Npgsql;
namespace StellaOps.Findings.Ledger.Observations;
/// <summary>
/// PostgreSQL implementation of observation repository.
/// </summary>
public sealed class PostgresObservationRepository : IObservationRepository
{
private readonly NpgsqlDataSource _dataSource;
private readonly TimeProvider _timeProvider;
private readonly ILogger<PostgresObservationRepository> _logger;
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
WriteIndented = false
};
public PostgresObservationRepository(
NpgsqlDataSource dataSource,
TimeProvider timeProvider,
ILogger<PostgresObservationRepository> logger)
{
_dataSource = dataSource;
_timeProvider = timeProvider;
_logger = logger;
}
/// <inheritdoc />
public async Task<Observation> CreateAsync(
Observation observation,
CancellationToken ct = default)
{
const string sql = """
INSERT INTO observations (
id, cve_id, product, tenant_id, finding_id, state, previous_state,
reason, user_id, evidence_ref, signals, created_at, expires_at
) VALUES (
@id, @cve_id, @product, @tenant_id, @finding_id, @state, @previous_state,
@reason, @user_id, @evidence_ref, @signals, @created_at, @expires_at
)
RETURNING *
""";
await using var conn = await _dataSource.OpenConnectionAsync(ct);
await using var cmd = new NpgsqlCommand(sql, conn);
cmd.Parameters.AddWithValue("id", observation.Id);
cmd.Parameters.AddWithValue("cve_id", observation.CveId);
cmd.Parameters.AddWithValue("product", observation.Product);
cmd.Parameters.AddWithValue("tenant_id", observation.TenantId);
cmd.Parameters.AddWithValue("finding_id", (object?)observation.FindingId ?? DBNull.Value);
cmd.Parameters.AddWithValue("state", observation.State.ToString().ToLowerInvariant());
cmd.Parameters.AddWithValue("previous_state",
(object?)observation.PreviousState?.ToString().ToLowerInvariant() ?? DBNull.Value);
cmd.Parameters.AddWithValue("reason", (object?)observation.Reason ?? DBNull.Value);
cmd.Parameters.AddWithValue("user_id", (object?)observation.UserId ?? DBNull.Value);
cmd.Parameters.AddWithValue("evidence_ref", (object?)observation.EvidenceRef ?? DBNull.Value);
cmd.Parameters.AddWithValue("signals", SerializeSignals(observation.Signals));
cmd.Parameters.AddWithValue("created_at", observation.CreatedAt);
cmd.Parameters.AddWithValue("expires_at", (object?)observation.ExpiresAt ?? DBNull.Value);
await using var reader = await cmd.ExecuteReaderAsync(ct);
if (await reader.ReadAsync(ct))
{
return MapFromReader(reader);
}
throw new InvalidOperationException("Insert did not return a row");
}
/// <inheritdoc />
public async Task<Observation?> GetByIdAsync(
string id,
CancellationToken ct = default)
{
const string sql = """
SELECT * FROM observations WHERE id = @id
""";
await using var conn = await _dataSource.OpenConnectionAsync(ct);
await using var cmd = new NpgsqlCommand(sql, conn);
cmd.Parameters.AddWithValue("id", id);
await using var reader = await cmd.ExecuteReaderAsync(ct);
if (await reader.ReadAsync(ct))
{
return MapFromReader(reader);
}
return null;
}
/// <inheritdoc />
public async Task<IReadOnlyList<Observation>> GetByCveAsync(
string cveId,
string tenantId,
CancellationToken ct = default)
{
const string sql = """
SELECT * FROM observations
WHERE cve_id = @cve_id AND tenant_id = @tenant_id
ORDER BY created_at DESC
""";
return await ExecuteQueryAsync(sql, new { cve_id = cveId, tenant_id = tenantId }, ct);
}
/// <inheritdoc />
public async Task<IReadOnlyList<Observation>> GetByProductAsync(
string product,
string tenantId,
CancellationToken ct = default)
{
const string sql = """
SELECT * FROM observations
WHERE product = @product AND tenant_id = @tenant_id
ORDER BY created_at DESC
""";
return await ExecuteQueryAsync(sql, new { product, tenant_id = tenantId }, ct);
}
/// <inheritdoc />
public async Task<IReadOnlyList<Observation>> GetByFindingAsync(
string findingId,
CancellationToken ct = default)
{
const string sql = """
SELECT * FROM observations
WHERE finding_id = @finding_id
ORDER BY created_at DESC
""";
return await ExecuteQueryAsync(sql, new { finding_id = findingId }, ct);
}
/// <inheritdoc />
public async Task<Observation?> GetLatestAsync(
string cveId,
string product,
string tenantId,
CancellationToken ct = default)
{
const string sql = """
SELECT * FROM observations
WHERE cve_id = @cve_id AND product = @product AND tenant_id = @tenant_id
ORDER BY created_at DESC
LIMIT 1
""";
var results = await ExecuteQueryAsync(sql, new { cve_id = cveId, product, tenant_id = tenantId }, ct);
return results.Count > 0 ? results[0] : null;
}
/// <inheritdoc />
public async Task<IReadOnlyList<Observation>> GetHistoryAsync(
string cveId,
string product,
string tenantId,
int limit = 100,
CancellationToken ct = default)
{
const string sql = """
SELECT * FROM observations
WHERE cve_id = @cve_id AND product = @product AND tenant_id = @tenant_id
ORDER BY created_at DESC
LIMIT @limit
""";
return await ExecuteQueryAsync(sql, new { cve_id = cveId, product, tenant_id = tenantId, limit }, ct);
}
/// <inheritdoc />
public async Task<IReadOnlyList<Observation>> GetByStateAsync(
ObservationState state,
string tenantId,
int limit = 100,
int offset = 0,
CancellationToken ct = default)
{
const string sql = """
SELECT * FROM observations
WHERE state = @state AND tenant_id = @tenant_id
ORDER BY created_at DESC
LIMIT @limit OFFSET @offset
""";
return await ExecuteQueryAsync(sql, new
{
state = state.ToString().ToLowerInvariant(),
tenant_id = tenantId,
limit,
offset
}, ct);
}
/// <inheritdoc />
public async Task<IReadOnlyList<Observation>> GetExpiringAsync(
DateTimeOffset before,
string tenantId,
CancellationToken ct = default)
{
const string sql = """
SELECT * FROM observations
WHERE expires_at IS NOT NULL
AND expires_at <= @before
AND tenant_id = @tenant_id
ORDER BY expires_at ASC
""";
return await ExecuteQueryAsync(sql, new { before, tenant_id = tenantId }, ct);
}
/// <inheritdoc />
public async Task<IDictionary<ObservationState, int>> CountByStateAsync(
string tenantId,
CancellationToken ct = default)
{
const string sql = """
SELECT state, COUNT(*) as count
FROM observations
WHERE tenant_id = @tenant_id
GROUP BY state
""";
await using var conn = await _dataSource.OpenConnectionAsync(ct);
await using var cmd = new NpgsqlCommand(sql, conn);
cmd.Parameters.AddWithValue("tenant_id", tenantId);
var result = new Dictionary<ObservationState, int>();
await using var reader = await cmd.ExecuteReaderAsync(ct);
while (await reader.ReadAsync(ct))
{
var stateStr = reader.GetString(0);
var count = reader.GetInt32(1);
if (Enum.TryParse<ObservationState>(stateStr, ignoreCase: true, out var state))
{
result[state] = count;
}
}
return result;
}
private async Task<IReadOnlyList<Observation>> ExecuteQueryAsync(
string sql,
object parameters,
CancellationToken ct)
{
var results = new List<Observation>();
await using var conn = await _dataSource.OpenConnectionAsync(ct);
await using var cmd = new NpgsqlCommand(sql, conn);
foreach (var prop in parameters.GetType().GetProperties())
{
var value = prop.GetValue(parameters);
cmd.Parameters.AddWithValue(prop.Name, value ?? DBNull.Value);
}
await using var reader = await cmd.ExecuteReaderAsync(ct);
while (await reader.ReadAsync(ct))
{
results.Add(MapFromReader(reader));
}
return results;
}
private static Observation MapFromReader(NpgsqlDataReader reader)
{
var previousStateOrdinal = reader.GetOrdinal("previous_state");
ObservationState? previousState = null;
if (!reader.IsDBNull(previousStateOrdinal))
{
var prevStr = reader.GetString(previousStateOrdinal);
if (Enum.TryParse<ObservationState>(prevStr, ignoreCase: true, out var ps))
{
previousState = ps;
}
}
return new Observation
{
Id = reader.GetString(reader.GetOrdinal("id")),
CveId = reader.GetString(reader.GetOrdinal("cve_id")),
Product = reader.GetString(reader.GetOrdinal("product")),
TenantId = reader.GetString(reader.GetOrdinal("tenant_id")),
FindingId = GetNullableString(reader, "finding_id"),
State = Enum.Parse<ObservationState>(
reader.GetString(reader.GetOrdinal("state")),
ignoreCase: true),
PreviousState = previousState,
Reason = GetNullableString(reader, "reason"),
UserId = GetNullableString(reader, "user_id"),
EvidenceRef = GetNullableString(reader, "evidence_ref"),
Signals = DeserializeSignals(reader),
CreatedAt = reader.GetFieldValue<DateTimeOffset>(reader.GetOrdinal("created_at")),
ExpiresAt = GetNullableDateTime(reader, "expires_at")
};
}
private static string? GetNullableString(NpgsqlDataReader reader, string column)
{
var ordinal = reader.GetOrdinal(column);
return reader.IsDBNull(ordinal) ? null : reader.GetString(ordinal);
}
private static DateTimeOffset? GetNullableDateTime(NpgsqlDataReader reader, string column)
{
var ordinal = reader.GetOrdinal(column);
return reader.IsDBNull(ordinal) ? null : reader.GetFieldValue<DateTimeOffset>(ordinal);
}
private static object SerializeSignals(SignalSnapshotSummary? signals)
{
if (signals is null) return DBNull.Value;
return JsonSerializer.Serialize(signals, JsonOptions);
}
private static SignalSnapshotSummary? DeserializeSignals(NpgsqlDataReader reader)
{
var ordinal = reader.GetOrdinal("signals");
if (reader.IsDBNull(ordinal)) return null;
var json = reader.GetString(ordinal);
return JsonSerializer.Deserialize<SignalSnapshotSummary>(json, JsonOptions);
}
}

View File

@@ -0,0 +1,389 @@
// -----------------------------------------------------------------------------
// ISignalSnapshotBuilder.cs
// Sprint: SPRINT_20260106_001_004_BE_determinization_integration
// Tasks: DBI-014, DBI-015, DBI-016, DBI-017 - Signal snapshot builder
// Description: Builds signal snapshots by fetching signals in parallel
// -----------------------------------------------------------------------------
using Microsoft.Extensions.Logging;
namespace StellaOps.Findings.Ledger.Observations;
/// <summary>
/// Builds signal snapshots by fetching signals in parallel.
/// </summary>
public interface ISignalSnapshotBuilder
{
/// <summary>
/// Builds a signal snapshot for a CVE/product pair.
/// </summary>
Task<SignalSnapshot> BuildAsync(
string cveId,
string product,
CancellationToken ct = default);
/// <summary>
/// Builds signal snapshots for multiple CVE/product pairs.
/// </summary>
Task<IReadOnlyDictionary<(string CveId, string Product), SignalSnapshot>> BuildBatchAsync(
IReadOnlyList<(string CveId, string Product)> pairs,
CancellationToken ct = default);
}
/// <summary>
/// Complete signal snapshot with all signal states.
/// </summary>
public sealed record SignalSnapshot
{
/// <summary>CVE ID.</summary>
public required string CveId { get; init; }
/// <summary>Product (purl or cpe).</summary>
public required string Product { get; init; }
/// <summary>EPSS signal state.</summary>
public required SignalResult Epss { get; init; }
/// <summary>KEV signal state.</summary>
public required SignalResult Kev { get; init; }
/// <summary>VEX signal state.</summary>
public required SignalResult Vex { get; init; }
/// <summary>Reachability signal state.</summary>
public required SignalResult Reachability { get; init; }
/// <summary>Exploit signal state.</summary>
public SignalResult? Exploit { get; init; }
/// <summary>When the snapshot was taken.</summary>
public required DateTimeOffset SnapshotAt { get; init; }
/// <summary>List of failed signals.</summary>
public IReadOnlyList<string> FailedSignals { get; init; } = [];
/// <summary>Computed uncertainty score.</summary>
public double UncertaintyScore => ComputeUncertainty();
private double ComputeUncertainty()
{
// Start with full uncertainty
var uncertainty = 1.0;
var signalWeight = 0.25; // 4 main signals
// Reduce uncertainty for each available signal
if (Epss.Status == SignalResultStatus.Available) uncertainty -= signalWeight * 0.9;
if (Kev.Status == SignalResultStatus.Available) uncertainty -= signalWeight * 0.95;
if (Vex.Status == SignalResultStatus.Available) uncertainty -= signalWeight * 0.85;
if (Reachability.Status == SignalResultStatus.Available) uncertainty -= signalWeight * 0.9;
return Math.Max(0.0, Math.Min(1.0, uncertainty));
}
}
/// <summary>
/// Result of fetching a signal.
/// </summary>
public sealed record SignalResult
{
/// <summary>Signal status.</summary>
public required SignalResultStatus Status { get; init; }
/// <summary>Summary value (for display).</summary>
public string? Summary { get; init; }
/// <summary>Raw value (JSON-serializable).</summary>
public object? RawValue { get; init; }
/// <summary>Source of the signal.</summary>
public string? Source { get; init; }
/// <summary>When captured.</summary>
public DateTimeOffset? CapturedAt { get; init; }
/// <summary>Error message if failed.</summary>
public string? Error { get; init; }
public static SignalResult Available(string summary, object? rawValue = null, string? source = null, DateTimeOffset? capturedAt = null)
=> new() { Status = SignalResultStatus.Available, Summary = summary, RawValue = rawValue, Source = source, CapturedAt = capturedAt };
public static SignalResult NotFound(string? source = null)
=> new() { Status = SignalResultStatus.NotFound, Source = source };
public static SignalResult Failed(string error, string? source = null)
=> new() { Status = SignalResultStatus.Failed, Error = error, Source = source };
public static SignalResult NotConfigured()
=> new() { Status = SignalResultStatus.NotConfigured };
}
/// <summary>
/// Signal result status.
/// </summary>
public enum SignalResultStatus
{
Available,
NotFound,
Failed,
NotConfigured
}
/// <summary>
/// Provider for a specific signal type.
/// </summary>
public interface ISignalProvider
{
/// <summary>Signal type name.</summary>
string SignalType { get; }
/// <summary>Fetches the signal.</summary>
Task<SignalResult> FetchAsync(string cveId, string? product, CancellationToken ct);
}
/// <summary>
/// Default implementation of signal snapshot builder.
/// </summary>
public sealed class SignalSnapshotBuilder : ISignalSnapshotBuilder
{
private readonly ISignalProvider? _epssProvider;
private readonly ISignalProvider? _kevProvider;
private readonly ISignalProvider? _vexProvider;
private readonly ISignalProvider? _reachabilityProvider;
private readonly TimeProvider _timeProvider;
private readonly ILogger<SignalSnapshotBuilder> _logger;
public SignalSnapshotBuilder(
IEnumerable<ISignalProvider> providers,
TimeProvider timeProvider,
ILogger<SignalSnapshotBuilder> logger)
{
var providerDict = providers.ToDictionary(p => p.SignalType, StringComparer.OrdinalIgnoreCase);
providerDict.TryGetValue("epss", out _epssProvider);
providerDict.TryGetValue("kev", out _kevProvider);
providerDict.TryGetValue("vex", out _vexProvider);
providerDict.TryGetValue("reachability", out _reachabilityProvider);
_timeProvider = timeProvider;
_logger = logger;
}
/// <inheritdoc />
public async Task<SignalSnapshot> BuildAsync(
string cveId,
string product,
CancellationToken ct = default)
{
var now = _timeProvider.GetUtcNow();
var failedSignals = new List<string>();
// Fetch all signals in parallel
var epssTask = FetchSignalAsync(_epssProvider, "epss", cveId, product, ct);
var kevTask = FetchSignalAsync(_kevProvider, "kev", cveId, product, ct);
var vexTask = FetchSignalAsync(_vexProvider, "vex", cveId, product, ct);
var reachabilityTask = FetchSignalAsync(_reachabilityProvider, "reachability", cveId, product, ct);
await Task.WhenAll(epssTask, kevTask, vexTask, reachabilityTask);
var epss = await epssTask;
var kev = await kevTask;
var vex = await vexTask;
var reachability = await reachabilityTask;
if (epss.Status == SignalResultStatus.Failed) failedSignals.Add("epss");
if (kev.Status == SignalResultStatus.Failed) failedSignals.Add("kev");
if (vex.Status == SignalResultStatus.Failed) failedSignals.Add("vex");
if (reachability.Status == SignalResultStatus.Failed) failedSignals.Add("reachability");
return new SignalSnapshot
{
CveId = cveId,
Product = product,
Epss = epss,
Kev = kev,
Vex = vex,
Reachability = reachability,
SnapshotAt = now,
FailedSignals = failedSignals
};
}
/// <inheritdoc />
public async Task<IReadOnlyDictionary<(string CveId, string Product), SignalSnapshot>> BuildBatchAsync(
IReadOnlyList<(string CveId, string Product)> pairs,
CancellationToken ct = default)
{
var tasks = pairs.Select(async pair =>
{
var snapshot = await BuildAsync(pair.CveId, pair.Product, ct);
return (pair, snapshot);
});
var results = await Task.WhenAll(tasks);
return results.ToDictionary(r => r.pair, r => r.snapshot);
}
private async Task<SignalResult> FetchSignalAsync(
ISignalProvider? provider,
string signalType,
string cveId,
string product,
CancellationToken ct)
{
if (provider is null)
{
return SignalResult.NotConfigured();
}
try
{
return await provider.FetchAsync(cveId, product, ct);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to fetch {SignalType} for {CveId}", signalType, cveId);
return SignalResult.Failed(ex.Message, signalType);
}
}
}
/// <summary>
/// EPSS signal provider adapter.
/// </summary>
public sealed class EpssSignalProvider : ISignalProvider
{
private readonly IEpssService _service;
public string SignalType => "epss";
public EpssSignalProvider(IEpssService service)
{
_service = service;
}
public async Task<SignalResult> FetchAsync(string cveId, string? product, CancellationToken ct)
{
var epss = await _service.GetEpssAsync(cveId, ct);
if (epss is null)
{
return SignalResult.NotFound("epss-feed");
}
var summary = $"Score: {epss.Score:P1}, Percentile: {epss.Percentile:P0}";
return SignalResult.Available(summary, epss, "epss-feed", epss.CapturedAt);
}
}
/// <summary>
/// KEV signal provider adapter.
/// </summary>
public sealed class KevSignalProvider : ISignalProvider
{
private readonly IKevService _service;
public string SignalType => "kev";
public KevSignalProvider(IKevService service)
{
_service = service;
}
public async Task<SignalResult> FetchAsync(string cveId, string? product, CancellationToken ct)
{
var isInKev = await _service.IsInKevAsync(cveId, ct);
var summary = isInKev ? "IN KEV - actively exploited" : "Not in KEV";
return SignalResult.Available(summary, new { IsInKev = isInKev }, "cisa-kev");
}
}
/// <summary>
/// VEX signal provider adapter.
/// </summary>
public sealed class VexSignalProvider : ISignalProvider
{
private readonly IVexService _service;
public string SignalType => "vex";
public VexSignalProvider(IVexService service)
{
_service = service;
}
public async Task<SignalResult> FetchAsync(string cveId, string? product, CancellationToken ct)
{
if (string.IsNullOrEmpty(product))
{
return SignalResult.NotFound("vex");
}
var vex = await _service.GetVexAsync(cveId, product, ct);
if (vex is null)
{
return SignalResult.NotFound("vex");
}
return SignalResult.Available(vex.Status, vex, "vex");
}
}
/// <summary>
/// Reachability signal provider adapter.
/// </summary>
public sealed class ReachabilitySignalProvider : ISignalProvider
{
private readonly IReachabilityService _service;
public string SignalType => "reachability";
public ReachabilitySignalProvider(IReachabilityService service)
{
_service = service;
}
public async Task<SignalResult> FetchAsync(string cveId, string? product, CancellationToken ct)
{
if (string.IsNullOrEmpty(product))
{
return SignalResult.NotFound("reachability");
}
var reachability = await _service.GetReachabilityAsync(cveId, product, ct);
if (reachability is null)
{
return SignalResult.NotFound("reachability");
}
return SignalResult.Available(reachability.Status, reachability, "reachability-graph");
}
}
// Service interfaces for signal providers (to be implemented by respective modules)
public interface IEpssService
{
Task<EpssResult?> GetEpssAsync(string cveId, CancellationToken ct);
}
public record EpssResult(double Score, double Percentile, DateTimeOffset CapturedAt);
public interface IKevService
{
Task<bool> IsInKevAsync(string cveId, CancellationToken ct);
}
public interface IVexService
{
Task<VexResult?> GetVexAsync(string cveId, string product, CancellationToken ct);
}
public record VexResult(string Status, string? Justification);
public interface IReachabilityService
{
Task<ReachabilityResult?> GetReachabilityAsync(string cveId, string product, CancellationToken ct);
}
public record ReachabilityResult(string Status, bool IsReachable);

View File

@@ -1,3 +1,4 @@
using System.Globalization;
using System.Text.Json.Nodes;
using Microsoft.Extensions.Logging;
using StellaOps.Findings.Ledger.Domain;
@@ -66,7 +67,7 @@ public sealed class AirgapImportService
["bundleId"] = input.BundleId,
["mirrorGeneration"] = input.MirrorGeneration,
["merkleRoot"] = input.MerkleRoot,
["timeAnchor"] = input.TimeAnchor.ToUniversalTime().ToString("O"),
["timeAnchor"] = input.TimeAnchor.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture),
["publisher"] = input.Publisher,
["hashAlgorithm"] = input.HashAlgorithm,
["contents"] = new JsonArray(input.Contents.Select(c => (JsonNode)c).ToArray())

View File

@@ -1,3 +1,4 @@
using System.Globalization;
using System.Text.Json.Nodes;
using Microsoft.Extensions.Logging;
using StellaOps.Findings.Ledger.Domain;
@@ -68,7 +69,7 @@ public sealed class AirgapTimelineService
["highDelta"] = impact.HighDelta,
["mediumDelta"] = impact.MediumDelta,
["lowDelta"] = impact.LowDelta,
["timeAnchor"] = input.TimeAnchor.ToString("O"),
["timeAnchor"] = input.TimeAnchor.ToString("O", CultureInfo.InvariantCulture),
["sealedMode"] = input.SealedMode
}
};

View File

@@ -1,3 +1,4 @@
using System.Globalization;
using System.Text.Json.Nodes;
using Microsoft.Extensions.Logging;
using StellaOps.Findings.Ledger.Domain;
@@ -80,7 +81,7 @@ public sealed class EvidenceSnapshotService
{
["bundleUri"] = input.BundleUri,
["dsseDigest"] = input.DsseDigest,
["expiresAt"] = expiresAt?.ToString("O")
["expiresAt"] = expiresAt?.ToString("O", CultureInfo.InvariantCulture)
}
}
};

View File

@@ -1,3 +1,4 @@
using System.Globalization;
using System.Text.Json.Nodes;
using Microsoft.Extensions.Logging;
using StellaOps.Findings.Ledger.Hashing;
@@ -74,8 +75,8 @@ public sealed class OrchestratorExportService
["jobType"] = input.JobType,
["artifactHash"] = input.ArtifactHash,
["policyHash"] = input.PolicyHash,
["startedAt"] = input.StartedAt.ToUniversalTime().ToString("O"),
["completedAt"] = input.CompletedAt?.ToUniversalTime().ToString("O"),
["startedAt"] = input.StartedAt.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture),
["completedAt"] = input.CompletedAt?.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture),
["status"] = input.Status,
["manifestPath"] = input.ManifestPath,
["logsPath"] = input.LogsPath

View File

@@ -1,3 +1,4 @@
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
@@ -91,13 +92,13 @@ public sealed class ScoredFindingsExportService : IScoredFindingsExportService
return new MemoryStream(result.Data);
}
private static byte[] ExportToJson(IReadOnlyList<ScoredFinding> findings, ScoredFindingsExportRequest request)
private byte[] ExportToJson(IReadOnlyList<ScoredFinding> findings, ScoredFindingsExportRequest request)
{
var envelope = new JsonObject
{
["version"] = "1.0",
["tenant_id"] = request.TenantId,
["generated_at"] = DateTimeOffset.UtcNow.ToString("O"),
["generated_at"] = _timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture),
["record_count"] = findings.Count,
["findings"] = new JsonArray(findings.Select(MapToJsonNode).ToArray())
};
@@ -130,7 +131,7 @@ public sealed class ScoredFindingsExportService : IScoredFindingsExportService
finding.RiskScore?.ToString("F4") ?? "",
EscapeCsv(finding.RiskSeverity ?? ""),
EscapeCsv(finding.RiskProfileVersion ?? ""),
finding.UpdatedAt.ToString("O")));
finding.UpdatedAt.ToString("O", CultureInfo.InvariantCulture)));
}
return Encoding.UTF8.GetBytes(sb.ToString());

View File

@@ -1,6 +1,7 @@
namespace StellaOps.Findings.Ledger.Services;
using System.Collections.Generic;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
@@ -422,10 +423,10 @@ public sealed class SnapshotService
metadata["incident.mode"] = "enabled";
metadata["incident.activationId"] = incident.ActivationId ?? string.Empty;
metadata["incident.retentionExtensionDays"] = incident.RetentionExtensionDays;
metadata["incident.changedAt"] = incident.ChangedAt.ToString("O");
metadata["incident.changedAt"] = incident.ChangedAt.ToString("O", CultureInfo.InvariantCulture);
if (incident.ExpiresAt is not null)
{
metadata["incident.expiresAt"] = incident.ExpiresAt.Value.ToString("O");
metadata["incident.expiresAt"] = incident.ExpiresAt.Value.ToString("O", CultureInfo.InvariantCulture);
}
_logger.LogInformation(

View File

@@ -0,0 +1,257 @@
// -----------------------------------------------------------------------------
// SignalSnapshotBuilderTests.cs
// Sprint: SPRINT_20260106_001_004_BE_determinization_integration
// Task: DBI-019 - Write unit tests for SignalSnapshotBuilder
// Description: Unit tests for parallel signal snapshot building
// -----------------------------------------------------------------------------
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Time.Testing;
using Moq;
using StellaOps.Findings.Ledger.Observations;
using Xunit;
namespace StellaOps.Findings.Ledger.Tests.Observations;
[Trait("Category", "Unit")]
public sealed class SignalSnapshotBuilderTests
{
private readonly FakeTimeProvider _timeProvider = new();
private readonly Mock<ISignalProvider> _epssMock = new();
private readonly Mock<ISignalProvider> _kevMock = new();
private readonly Mock<ISignalProvider> _vexMock = new();
private readonly Mock<ISignalProvider> _reachabilityMock = new();
public SignalSnapshotBuilderTests()
{
_timeProvider.SetUtcNow(new DateTimeOffset(2026, 1, 7, 12, 0, 0, TimeSpan.Zero));
_epssMock.Setup(x => x.SignalType).Returns("epss");
_kevMock.Setup(x => x.SignalType).Returns("kev");
_vexMock.Setup(x => x.SignalType).Returns("vex");
_reachabilityMock.Setup(x => x.SignalType).Returns("reachability");
}
private SignalSnapshotBuilder CreateBuilder(params ISignalProvider[] providers)
{
return new SignalSnapshotBuilder(
providers,
_timeProvider,
NullLogger<SignalSnapshotBuilder>.Instance);
}
[Fact]
public async Task BuildAsync_FetchesAllSignalsInParallel()
{
// Arrange
var cveId = "CVE-2024-1234";
var product = "pkg:npm/lodash@4.17.0";
_epssMock
.Setup(x => x.FetchAsync(cveId, product, It.IsAny<CancellationToken>()))
.ReturnsAsync(SignalResult.Available("Score: 0.85", new { Score = 0.85 }));
_kevMock
.Setup(x => x.FetchAsync(cveId, product, It.IsAny<CancellationToken>()))
.ReturnsAsync(SignalResult.Available("IN KEV", new { IsInKev = true }));
_vexMock
.Setup(x => x.FetchAsync(cveId, product, It.IsAny<CancellationToken>()))
.ReturnsAsync(SignalResult.Available("not_affected"));
_reachabilityMock
.Setup(x => x.FetchAsync(cveId, product, It.IsAny<CancellationToken>()))
.ReturnsAsync(SignalResult.Available("Reachable"));
var builder = CreateBuilder(_epssMock.Object, _kevMock.Object, _vexMock.Object, _reachabilityMock.Object);
// Act
var snapshot = await builder.BuildAsync(cveId, product);
// Assert
Assert.Equal(cveId, snapshot.CveId);
Assert.Equal(product, snapshot.Product);
Assert.Equal(SignalResultStatus.Available, snapshot.Epss.Status);
Assert.Equal(SignalResultStatus.Available, snapshot.Kev.Status);
Assert.Equal(SignalResultStatus.Available, snapshot.Vex.Status);
Assert.Equal(SignalResultStatus.Available, snapshot.Reachability.Status);
Assert.Empty(snapshot.FailedSignals);
_epssMock.Verify(x => x.FetchAsync(cveId, product, It.IsAny<CancellationToken>()), Times.Once);
_kevMock.Verify(x => x.FetchAsync(cveId, product, It.IsAny<CancellationToken>()), Times.Once);
_vexMock.Verify(x => x.FetchAsync(cveId, product, It.IsAny<CancellationToken>()), Times.Once);
_reachabilityMock.Verify(x => x.FetchAsync(cveId, product, It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task BuildAsync_MissingProvider_ReturnsNotConfigured()
{
// Arrange - only EPSS provider available
_epssMock
.Setup(x => x.FetchAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(SignalResult.Available("Score: 0.5"));
var builder = CreateBuilder(_epssMock.Object);
// Act
var snapshot = await builder.BuildAsync("CVE-2024-1234", "pkg:npm/test@1.0.0");
// Assert
Assert.Equal(SignalResultStatus.Available, snapshot.Epss.Status);
Assert.Equal(SignalResultStatus.NotConfigured, snapshot.Kev.Status);
Assert.Equal(SignalResultStatus.NotConfigured, snapshot.Vex.Status);
Assert.Equal(SignalResultStatus.NotConfigured, snapshot.Reachability.Status);
}
[Fact]
public async Task BuildAsync_ProviderThrows_ReturnsFailed()
{
// Arrange
_epssMock
.Setup(x => x.FetchAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ThrowsAsync(new InvalidOperationException("Connection refused"));
var builder = CreateBuilder(_epssMock.Object);
// Act
var snapshot = await builder.BuildAsync("CVE-2024-1234", "pkg:npm/test@1.0.0");
// Assert
Assert.Equal(SignalResultStatus.Failed, snapshot.Epss.Status);
Assert.Equal("Connection refused", snapshot.Epss.Error);
Assert.Contains("epss", snapshot.FailedSignals);
}
[Fact]
public async Task BuildAsync_ProviderNotFound_ReturnsNotFound()
{
// Arrange
_epssMock
.Setup(x => x.FetchAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(SignalResult.NotFound("epss-feed"));
var builder = CreateBuilder(_epssMock.Object);
// Act
var snapshot = await builder.BuildAsync("CVE-2099-9999", "pkg:npm/test@1.0.0");
// Assert
Assert.Equal(SignalResultStatus.NotFound, snapshot.Epss.Status);
Assert.DoesNotContain("epss", snapshot.FailedSignals); // NotFound is not a failure
}
[Fact]
public async Task BuildAsync_ComputesUncertaintyScore()
{
// Arrange - all signals available = low uncertainty
SetupAllSignalsAvailable();
var builder = CreateBuilder(_epssMock.Object, _kevMock.Object, _vexMock.Object, _reachabilityMock.Object);
// Act
var snapshot = await builder.BuildAsync("CVE-2024-1234", "pkg:npm/test@1.0.0");
// Assert - with all signals, uncertainty should be low
Assert.True(snapshot.UncertaintyScore < 0.2);
}
[Fact]
public async Task BuildAsync_NoSignals_HighUncertainty()
{
// Arrange - no providers configured
var builder = CreateBuilder();
// Act
var snapshot = await builder.BuildAsync("CVE-2024-1234", "pkg:npm/test@1.0.0");
// Assert - with no signals, uncertainty should be high
Assert.Equal(1.0, snapshot.UncertaintyScore);
}
[Fact]
public async Task BuildAsync_SnapshotAtUsesTimeProvider()
{
// Arrange
var expectedTime = new DateTimeOffset(2026, 1, 7, 12, 0, 0, TimeSpan.Zero);
var builder = CreateBuilder();
// Act
var snapshot = await builder.BuildAsync("CVE-2024-1234", "pkg:npm/test@1.0.0");
// Assert
Assert.Equal(expectedTime, snapshot.SnapshotAt);
}
[Fact]
public async Task BuildBatchAsync_ProcessesMultiplePairs()
{
// Arrange
SetupAllSignalsAvailable();
var builder = CreateBuilder(_epssMock.Object, _kevMock.Object, _vexMock.Object, _reachabilityMock.Object);
var pairs = new List<(string CveId, string Product)>
{
("CVE-2024-0001", "pkg:npm/foo@1.0.0"),
("CVE-2024-0002", "pkg:npm/bar@2.0.0"),
("CVE-2024-0003", "pkg:npm/baz@3.0.0")
};
// Act
var results = await builder.BuildBatchAsync(pairs);
// Assert
Assert.Equal(3, results.Count);
Assert.All(pairs, pair => Assert.True(results.ContainsKey(pair)));
}
[Fact]
public async Task BuildAsync_TracksFailedSignals()
{
// Arrange
_epssMock
.Setup(x => x.FetchAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ThrowsAsync(new Exception("EPSS error"));
_kevMock
.Setup(x => x.FetchAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(SignalResult.Available("Not in KEV"));
_vexMock
.Setup(x => x.FetchAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ThrowsAsync(new Exception("VEX error"));
_reachabilityMock
.Setup(x => x.FetchAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(SignalResult.Available("Reachable"));
var builder = CreateBuilder(_epssMock.Object, _kevMock.Object, _vexMock.Object, _reachabilityMock.Object);
// Act
var snapshot = await builder.BuildAsync("CVE-2024-1234", "pkg:npm/test@1.0.0");
// Assert
Assert.Equal(2, snapshot.FailedSignals.Count);
Assert.Contains("epss", snapshot.FailedSignals);
Assert.Contains("vex", snapshot.FailedSignals);
}
private void SetupAllSignalsAvailable()
{
_epssMock
.Setup(x => x.FetchAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(SignalResult.Available("Score: 0.5"));
_kevMock
.Setup(x => x.FetchAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(SignalResult.Available("Not in KEV"));
_vexMock
.Setup(x => x.FetchAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(SignalResult.Available("not_affected"));
_reachabilityMock
.Setup(x => x.FetchAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(SignalResult.Available("Reachable"));
}
}

View File

@@ -1,5 +1,6 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
using System.Text.Json;
namespace LedgerReplayHarness;
@@ -117,7 +118,7 @@ public sealed class HarnessRunner
cpuPercentMax = 0,
memoryMbMax = 0,
status = hashesValid && merkleOk ? "pass" : "fail",
timestamp = _timeProvider.GetUtcNow().ToString("O"),
timestamp = _timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture),
hashSummary = stats.ToReport(),
merkleRoot = computedRoot,
merkleExpected = expectedMerkleRoot