audit work, fixed StellaOps.sln warnings/errors, fixed tests, sprints work, new advisories
This commit is contained in:
29
src/VexLens/AGENTS.md
Normal file
29
src/VexLens/AGENTS.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# VexLens Module Charter
|
||||
|
||||
## Mission
|
||||
- Compute deterministic consensus over VEX statements and expose conflict-aware APIs.
|
||||
|
||||
## Responsibilities
|
||||
- Implement consensus algorithm and conflict detection.
|
||||
- Persist consensus history and emit deterministic exports.
|
||||
- Expose APIs for consensus, conflicts, and trust weight updates.
|
||||
- Maintain offline bundle formats for Export Center.
|
||||
|
||||
## Required Reading
|
||||
- docs/README.md
|
||||
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
|
||||
- docs/modules/platform/architecture-overview.md
|
||||
- docs/modules/vex-lens/architecture.md
|
||||
- docs/modules/excititor/architecture.md
|
||||
|
||||
## Working Agreement
|
||||
- Deterministic ordering: sort by timestamps, trust tier, and confidence.
|
||||
- Use TimeProvider and IGuidGenerator; UTC timestamps.
|
||||
- Use InvariantCulture for formatting.
|
||||
- Propagate CancellationToken in async flows.
|
||||
- Preserve provenance fields in outputs.
|
||||
|
||||
## Testing Strategy
|
||||
- Unit tests for consensus join, conflicts, and precedence.
|
||||
- Integration tests for API endpoints and exports.
|
||||
- Determinism tests for repeatable consensus and bundle outputs.
|
||||
@@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -269,7 +270,7 @@ public sealed class PostgresConsensusProjectionStore : IConsensusProjectionStore
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var activity = ActivitySource.StartActivity("PurgeAsync");
|
||||
activity?.SetTag("olderThan", olderThan.ToString("O"));
|
||||
activity?.SetTag("olderThan", olderThan.ToString("O", CultureInfo.InvariantCulture));
|
||||
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(cancellationToken);
|
||||
await using var cmd = new NpgsqlCommand(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using StellaOps.VexLens.Consensus;
|
||||
using StellaOps.VexLens.Models;
|
||||
using StellaOps.VexLens.Storage;
|
||||
using ModelsVexStatus = StellaOps.VexLens.Models.VexStatus;
|
||||
using ModelsVexJustification = StellaOps.VexLens.Models.VexJustification;
|
||||
|
||||
namespace StellaOps.VexLens.Integration;
|
||||
|
||||
@@ -48,8 +49,8 @@ public sealed class PolicyEngineIntegration : IPolicyEngineIntegration
|
||||
VulnerabilityId: vulnerabilityId,
|
||||
ProductKey: productKey,
|
||||
HasVexData: true,
|
||||
Status: projection.Status,
|
||||
Justification: projection.Justification,
|
||||
Status: ConvertStatus(projection.Status),
|
||||
Justification: ConvertJustification(projection.Justification),
|
||||
ConfidenceScore: projection.ConfidenceScore,
|
||||
MeetsConfidenceThreshold: meetsThreshold,
|
||||
ProjectionId: projection.ProjectionId,
|
||||
@@ -190,6 +191,45 @@ public sealed class PolicyEngineIntegration : IPolicyEngineIntegration
|
||||
VexStatus: statusResult.Status,
|
||||
AdjustmentReason: $"{reason} (confidence: {confidenceScale:P0})");
|
||||
}
|
||||
|
||||
private static VexStatus? ConvertStatus(ModelsVexStatus? status)
|
||||
{
|
||||
if (!status.HasValue) return null;
|
||||
return status.Value switch
|
||||
{
|
||||
ModelsVexStatus.Affected => VexStatus.Affected,
|
||||
ModelsVexStatus.NotAffected => VexStatus.NotAffected,
|
||||
ModelsVexStatus.Fixed => VexStatus.Fixed,
|
||||
ModelsVexStatus.UnderInvestigation => VexStatus.UnderInvestigation,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private static VexStatus ConvertStatusNonNull(ModelsVexStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
ModelsVexStatus.Affected => VexStatus.Affected,
|
||||
ModelsVexStatus.NotAffected => VexStatus.NotAffected,
|
||||
ModelsVexStatus.Fixed => VexStatus.Fixed,
|
||||
ModelsVexStatus.UnderInvestigation => VexStatus.UnderInvestigation,
|
||||
_ => VexStatus.UnderInvestigation
|
||||
};
|
||||
}
|
||||
|
||||
private static VexJustification? ConvertJustification(ModelsVexJustification? justification)
|
||||
{
|
||||
if (!justification.HasValue) return null;
|
||||
return justification.Value switch
|
||||
{
|
||||
ModelsVexJustification.ComponentNotPresent => VexJustification.ComponentNotPresent,
|
||||
ModelsVexJustification.VulnerableCodeNotPresent => VexJustification.VulnerableCodeNotPresent,
|
||||
ModelsVexJustification.VulnerableCodeNotInExecutePath => VexJustification.VulnerableCodeNotInExecutePath,
|
||||
ModelsVexJustification.VulnerableCodeCannotBeControlledByAdversary => VexJustification.VulnerableCodeCannotBeControlledByAdversary,
|
||||
ModelsVexJustification.InlineMitigationsAlreadyExist => VexJustification.InlineMitigationsAlreadyExist,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -247,8 +287,8 @@ public sealed class VulnExplorerIntegration : IVulnExplorerIntegration
|
||||
.Select(p => new ProductVexStatus(
|
||||
ProductKey: p.ProductKey,
|
||||
ProductName: null,
|
||||
Status: p.Status,
|
||||
Justification: p.Justification,
|
||||
Status: ConvertStatusNonNull(p.Status),
|
||||
Justification: ConvertJustification(p.Justification),
|
||||
ConfidenceScore: p.ConfidenceScore,
|
||||
PrimaryIssuer: null,
|
||||
ComputedAt: p.ComputedAt))
|
||||
@@ -296,22 +336,23 @@ public sealed class VulnExplorerIntegration : IVulnExplorerIntegration
|
||||
|
||||
foreach (var projection in history.OrderBy(p => p.ComputedAt))
|
||||
{
|
||||
var currentStatus = ConvertStatusNonNull(projection.Status);
|
||||
var eventType = previousStatus == null
|
||||
? "initial"
|
||||
: projection.Status != previousStatus
|
||||
: currentStatus != previousStatus
|
||||
? "status_change"
|
||||
: "update";
|
||||
|
||||
entries.Add(new VexTimelineEntry(
|
||||
Timestamp: projection.ComputedAt,
|
||||
Status: projection.Status,
|
||||
Justification: projection.Justification,
|
||||
Status: currentStatus,
|
||||
Justification: ConvertJustification(projection.Justification),
|
||||
IssuerId: null,
|
||||
IssuerName: null,
|
||||
EventType: eventType,
|
||||
Notes: projection.RationaleSummary));
|
||||
|
||||
previousStatus = projection.Status;
|
||||
previousStatus = currentStatus;
|
||||
}
|
||||
|
||||
var statusChangeCount = entries.Count(e => e.EventType == "status_change");
|
||||
@@ -320,7 +361,7 @@ public sealed class VulnExplorerIntegration : IVulnExplorerIntegration
|
||||
VulnerabilityId: vulnerabilityId,
|
||||
ProductKey: productKey,
|
||||
Entries: entries,
|
||||
CurrentStatus: history.FirstOrDefault()?.Status,
|
||||
CurrentStatus: history.FirstOrDefault() is { } first ? ConvertStatusNonNull(first.Status) : null,
|
||||
StatusChangeCount: statusChangeCount);
|
||||
}
|
||||
|
||||
@@ -361,12 +402,14 @@ public sealed class VulnExplorerIntegration : IVulnExplorerIntegration
|
||||
}
|
||||
|
||||
var statusCounts = result.Projections
|
||||
.GroupBy(p => p.Status)
|
||||
.GroupBy(p => ConvertStatusNonNull(p.Status))
|
||||
.ToDictionary(g => g.Key, g => g.Count());
|
||||
|
||||
var justificationCounts = result.Projections
|
||||
.Where(p => p.Justification.HasValue)
|
||||
.GroupBy(p => p.Justification!.Value)
|
||||
.Select(p => ConvertJustification(p.Justification!.Value))
|
||||
.Where(j => j.HasValue)
|
||||
.GroupBy(j => j!.Value)
|
||||
.ToDictionary(g => g.Key, g => g.Count());
|
||||
|
||||
var totalStatements = result.Projections.Sum(p => p.StatementCount);
|
||||
@@ -396,7 +439,7 @@ public sealed class VulnExplorerIntegration : IVulnExplorerIntegration
|
||||
TenantId: context.TenantId,
|
||||
VulnerabilityId: searchQuery.VulnerabilityIdPattern,
|
||||
ProductKey: searchQuery.ProductKeyPattern,
|
||||
Status: searchQuery.Status,
|
||||
Status: ConvertStatusToModels(searchQuery.Status),
|
||||
Outcome: null,
|
||||
MinimumConfidence: searchQuery.MinimumConfidence,
|
||||
ComputedAfter: searchQuery.UpdatedAfter,
|
||||
@@ -412,8 +455,8 @@ public sealed class VulnExplorerIntegration : IVulnExplorerIntegration
|
||||
var hits = result.Projections.Select(p => new VexSearchHit(
|
||||
VulnerabilityId: p.VulnerabilityId,
|
||||
ProductKey: p.ProductKey,
|
||||
Status: p.Status,
|
||||
Justification: p.Justification,
|
||||
Status: ConvertStatusNonNull(p.Status),
|
||||
Justification: ConvertJustification(p.Justification),
|
||||
ConfidenceScore: p.ConfidenceScore,
|
||||
PrimaryIssuer: null,
|
||||
ComputedAt: p.ComputedAt)).ToList();
|
||||
@@ -424,4 +467,56 @@ public sealed class VulnExplorerIntegration : IVulnExplorerIntegration
|
||||
Offset: result.Offset,
|
||||
Limit: result.Limit);
|
||||
}
|
||||
|
||||
private static VexStatus? ConvertStatus(ModelsVexStatus? status)
|
||||
{
|
||||
if (!status.HasValue) return null;
|
||||
return status.Value switch
|
||||
{
|
||||
ModelsVexStatus.Affected => VexStatus.Affected,
|
||||
ModelsVexStatus.NotAffected => VexStatus.NotAffected,
|
||||
ModelsVexStatus.Fixed => VexStatus.Fixed,
|
||||
ModelsVexStatus.UnderInvestigation => VexStatus.UnderInvestigation,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private static VexStatus ConvertStatusNonNull(ModelsVexStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
ModelsVexStatus.Affected => VexStatus.Affected,
|
||||
ModelsVexStatus.NotAffected => VexStatus.NotAffected,
|
||||
ModelsVexStatus.Fixed => VexStatus.Fixed,
|
||||
ModelsVexStatus.UnderInvestigation => VexStatus.UnderInvestigation,
|
||||
_ => VexStatus.UnderInvestigation
|
||||
};
|
||||
}
|
||||
|
||||
private static ModelsVexStatus? ConvertStatusToModels(VexStatus? status)
|
||||
{
|
||||
if (!status.HasValue) return null;
|
||||
return status.Value switch
|
||||
{
|
||||
VexStatus.Affected => ModelsVexStatus.Affected,
|
||||
VexStatus.NotAffected => ModelsVexStatus.NotAffected,
|
||||
VexStatus.Fixed => ModelsVexStatus.Fixed,
|
||||
VexStatus.UnderInvestigation => ModelsVexStatus.UnderInvestigation,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private static VexJustification? ConvertJustification(ModelsVexJustification? justification)
|
||||
{
|
||||
if (!justification.HasValue) return null;
|
||||
return justification.Value switch
|
||||
{
|
||||
ModelsVexJustification.ComponentNotPresent => VexJustification.ComponentNotPresent,
|
||||
ModelsVexJustification.VulnerableCodeNotPresent => VexJustification.VulnerableCodeNotPresent,
|
||||
ModelsVexJustification.VulnerableCodeNotInExecutePath => VexJustification.VulnerableCodeNotInExecutePath,
|
||||
ModelsVexJustification.VulnerableCodeCannotBeControlledByAdversary => VexJustification.VulnerableCodeCannotBeControlledByAdversary,
|
||||
ModelsVexJustification.InlineMitigationsAlreadyExist => VexJustification.InlineMitigationsAlreadyExist,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
292
src/VexLens/StellaOps.VexLens/Integration/VexSignalEmitter.cs
Normal file
292
src/VexLens/StellaOps.VexLens/Integration/VexSignalEmitter.cs
Normal file
@@ -0,0 +1,292 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// VexSignalEmitter.cs
|
||||
// Sprint: SPRINT_20260106_001_004_BE_determinization_integration
|
||||
// Tasks: DBI-005, DBI-006, DBI-007 - VEX signal emitter and mapper
|
||||
// Description: Emits VEX signals to the determinization pipeline
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace StellaOps.VexLens.Integration;
|
||||
|
||||
/// <summary>
|
||||
/// VEX signal for the determinization pipeline.
|
||||
/// </summary>
|
||||
public sealed record VexSignal
|
||||
{
|
||||
/// <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>VEX status (affected, not_affected, fixed, under_investigation).</summary>
|
||||
public required VexStatus Status { get; init; }
|
||||
|
||||
/// <summary>Justification (if not_affected).</summary>
|
||||
public VexJustification? Justification { get; init; }
|
||||
|
||||
/// <summary>Impact statement.</summary>
|
||||
public string? ImpactStatement { get; init; }
|
||||
|
||||
/// <summary>Action statement.</summary>
|
||||
public string? ActionStatement { get; init; }
|
||||
|
||||
/// <summary>Version range affected.</summary>
|
||||
public string? VersionRange { get; init; }
|
||||
|
||||
/// <summary>VEX document ID.</summary>
|
||||
public required string DocumentId { get; init; }
|
||||
|
||||
/// <summary>VEX statement timestamp.</summary>
|
||||
public required DateTimeOffset Timestamp { get; init; }
|
||||
|
||||
/// <summary>Supplier/author of the VEX.</summary>
|
||||
public string? Supplier { get; init; }
|
||||
|
||||
/// <summary>Trust score of the VEX source (0.0-1.0).</summary>
|
||||
public double TrustScore { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// VEX status values.
|
||||
/// </summary>
|
||||
public enum VexStatus
|
||||
{
|
||||
/// <summary>Affected by the vulnerability.</summary>
|
||||
Affected,
|
||||
|
||||
/// <summary>Not affected by the vulnerability.</summary>
|
||||
NotAffected,
|
||||
|
||||
/// <summary>Fixed in this or a later version.</summary>
|
||||
Fixed,
|
||||
|
||||
/// <summary>Under investigation.</summary>
|
||||
UnderInvestigation
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Justification for not_affected status.
|
||||
/// </summary>
|
||||
public enum VexJustification
|
||||
{
|
||||
/// <summary>Component is not present.</summary>
|
||||
ComponentNotPresent,
|
||||
|
||||
/// <summary>Vulnerable code is not present.</summary>
|
||||
VulnerableCodeNotPresent,
|
||||
|
||||
/// <summary>Vulnerable code is not in execute path.</summary>
|
||||
VulnerableCodeNotInExecutePath,
|
||||
|
||||
/// <summary>Vulnerable code cannot be controlled by adversary.</summary>
|
||||
VulnerableCodeCannotBeControlledByAdversary,
|
||||
|
||||
/// <summary>Inline mitigations already exist.</summary>
|
||||
InlineMitigationsAlreadyExist
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event emitted when VEX status changes.
|
||||
/// </summary>
|
||||
public sealed record VexStatusChangedEvent
|
||||
{
|
||||
/// <summary>The CVE ID.</summary>
|
||||
public required string CveId { get; init; }
|
||||
|
||||
/// <summary>The product.</summary>
|
||||
public required string Product { get; init; }
|
||||
|
||||
/// <summary>Previous status.</summary>
|
||||
public VexStatus? PreviousStatus { get; init; }
|
||||
|
||||
/// <summary>New status.</summary>
|
||||
public required VexStatus NewStatus { get; init; }
|
||||
|
||||
/// <summary>VEX document ID.</summary>
|
||||
public required string DocumentId { get; init; }
|
||||
|
||||
/// <summary>When the change occurred.</summary>
|
||||
public required DateTimeOffset ChangedAt { get; init; }
|
||||
|
||||
/// <summary>Correlation ID for tracing.</summary>
|
||||
public string? CorrelationId { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Emits VEX signals for downstream processing.
|
||||
/// </summary>
|
||||
public interface IVexSignalEmitter
|
||||
{
|
||||
/// <summary>
|
||||
/// Emits a VEX signal.
|
||||
/// </summary>
|
||||
Task EmitAsync(VexSignal signal, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Emits VEX signals in batch.
|
||||
/// </summary>
|
||||
Task EmitBatchAsync(IReadOnlyList<VexSignal> signals, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Emits a VEX status change event.
|
||||
/// </summary>
|
||||
Task EmitStatusChangeAsync(VexStatusChangedEvent @event, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default VEX signal emitter.
|
||||
/// </summary>
|
||||
public sealed class VexSignalEmitter : IVexSignalEmitter
|
||||
{
|
||||
private readonly IVexSignalStore _store;
|
||||
private readonly IVexEventPublisher _publisher;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly ILogger<VexSignalEmitter> _logger;
|
||||
|
||||
public VexSignalEmitter(
|
||||
IVexSignalStore store,
|
||||
IVexEventPublisher publisher,
|
||||
TimeProvider timeProvider,
|
||||
ILogger<VexSignalEmitter> logger)
|
||||
{
|
||||
_store = store;
|
||||
_publisher = publisher;
|
||||
_timeProvider = timeProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task EmitAsync(VexSignal signal, CancellationToken ct = default)
|
||||
{
|
||||
// Get previous state if exists
|
||||
var previous = await _store.GetLatestAsync(signal.CveId, signal.Product, ct);
|
||||
|
||||
// Store new signal
|
||||
await _store.StoreAsync(signal, ct);
|
||||
|
||||
_logger.LogDebug(
|
||||
"VEX signal emitted: {CveId} on {Product} = {Status}",
|
||||
signal.CveId, signal.Product, signal.Status);
|
||||
|
||||
// Emit change event if status changed
|
||||
if (previous is null || previous.Status != signal.Status)
|
||||
{
|
||||
var @event = new VexStatusChangedEvent
|
||||
{
|
||||
CveId = signal.CveId,
|
||||
Product = signal.Product,
|
||||
PreviousStatus = previous?.Status,
|
||||
NewStatus = signal.Status,
|
||||
DocumentId = signal.DocumentId,
|
||||
ChangedAt = _timeProvider.GetUtcNow()
|
||||
};
|
||||
|
||||
await EmitStatusChangeAsync(@event, ct);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task EmitBatchAsync(IReadOnlyList<VexSignal> signals, CancellationToken ct = default)
|
||||
{
|
||||
foreach (var signal in signals)
|
||||
{
|
||||
await EmitAsync(signal, ct);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task EmitStatusChangeAsync(VexStatusChangedEvent @event, CancellationToken ct = default)
|
||||
{
|
||||
await _publisher.PublishAsync(@event, ct);
|
||||
|
||||
_logger.LogInformation(
|
||||
"VEX status changed: {CveId} on {Product}: {PreviousStatus} -> {NewStatus}",
|
||||
@event.CveId, @event.Product, @event.PreviousStatus, @event.NewStatus);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Store for VEX signals.
|
||||
/// </summary>
|
||||
public interface IVexSignalStore
|
||||
{
|
||||
/// <summary>
|
||||
/// Stores a VEX signal.
|
||||
/// </summary>
|
||||
Task StoreAsync(VexSignal signal, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the latest VEX signal for a CVE/product pair.
|
||||
/// </summary>
|
||||
Task<VexSignal?> GetLatestAsync(string cveId, string product, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all VEX signals for a CVE.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<VexSignal>> GetByCveAsync(string cveId, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all VEX signals for a product.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<VexSignal>> GetByProductAsync(string product, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Publisher for VEX events.
|
||||
/// </summary>
|
||||
public interface IVexEventPublisher
|
||||
{
|
||||
/// <summary>
|
||||
/// Publishes a VEX status change event.
|
||||
/// </summary>
|
||||
Task PublishAsync(VexStatusChangedEvent @event, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps VEX claims to summary format.
|
||||
/// </summary>
|
||||
public static class VexClaimSummaryMapper
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps a VEX signal to a claim summary.
|
||||
/// </summary>
|
||||
public static VexClaimSummary ToSummary(VexSignal signal)
|
||||
{
|
||||
return new VexClaimSummary
|
||||
{
|
||||
CveId = signal.CveId,
|
||||
Product = signal.Product,
|
||||
Status = signal.Status.ToString().ToLowerInvariant(),
|
||||
Justification = signal.Justification?.ToString(),
|
||||
ImpactStatement = signal.ImpactStatement,
|
||||
DocumentId = signal.DocumentId,
|
||||
Timestamp = signal.Timestamp,
|
||||
TrustScore = signal.TrustScore
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps multiple VEX signals to claim summaries.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<VexClaimSummary> ToSummaries(IEnumerable<VexSignal> signals)
|
||||
{
|
||||
return signals.Select(ToSummary).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Summary of a VEX claim for display/export.
|
||||
/// </summary>
|
||||
public sealed record VexClaimSummary
|
||||
{
|
||||
public required string CveId { get; init; }
|
||||
public required string Product { get; init; }
|
||||
public required string Status { get; init; }
|
||||
public string? Justification { get; init; }
|
||||
public string? ImpactStatement { get; init; }
|
||||
public required string DocumentId { get; init; }
|
||||
public required DateTimeOffset Timestamp { get; init; }
|
||||
public double TrustScore { get; init; }
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using StellaOps.VexLens.Consensus;
|
||||
using StellaOps.VexLens.Export;
|
||||
@@ -285,7 +286,7 @@ public sealed class ConsensusJobService : IConsensusJobService
|
||||
JobType: ConsensusJobTypes.ProjectionRefresh,
|
||||
TenantId: tenantId,
|
||||
Priority: ConsensusJobTypes.GetDefaultPriority(ConsensusJobTypes.ProjectionRefresh),
|
||||
IdempotencyKey: $"refresh:{tenantId}:{since?.ToString("O") ?? "all"}:{status?.ToString() ?? "all"}",
|
||||
IdempotencyKey: $"refresh:{tenantId}:{since?.ToString("O", CultureInfo.InvariantCulture) ?? "all"}:{status?.ToString() ?? "all"}",
|
||||
Payload: JsonSerializer.Serialize(payload, JsonOptions));
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Attestor.Core.Delta;
|
||||
@@ -293,7 +294,7 @@ public sealed class VexDeltaComputeService : IVexDeltaComputeService
|
||||
context.ArtifactDigest,
|
||||
context.PreviousStatus,
|
||||
context.NewStatus,
|
||||
context.ComputedAt.ToUniversalTime().ToString("O"));
|
||||
context.ComputedAt.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture));
|
||||
|
||||
var bytes = System.Text.Encoding.UTF8.GetBytes(input);
|
||||
var hash = System.Security.Cryptography.SHA256.HashData(bytes);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// © StellaOps Contributors. See LICENSE and NOTICE.md in the repository root.
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.VexLens.Consensus;
|
||||
@@ -207,7 +208,7 @@ public sealed class DualWriteConsensusProjectionStore : IConsensusProjectionStor
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var activity = ActivitySource.StartActivity("PurgeAsync.DualWrite");
|
||||
activity?.SetTag("olderThan", olderThan.ToString("O"));
|
||||
activity?.SetTag("olderThan", olderThan.ToString("O", CultureInfo.InvariantCulture));
|
||||
|
||||
// Purge from both stores
|
||||
var primaryCount = await PrimaryStore.PurgeAsync(olderThan, tenantId, cancellationToken);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// © StellaOps Contributors. See LICENSE and NOTICE.md in the repository root.
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Determinism;
|
||||
@@ -325,7 +326,7 @@ public sealed class PostgresConsensusProjectionStoreProxy : IConsensusProjection
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var activity = ActivitySource.StartActivity("PurgeAsync");
|
||||
activity?.SetTag("olderThan", olderThan.ToString("O"));
|
||||
activity?.SetTag("olderThan", olderThan.ToString("O", CultureInfo.InvariantCulture));
|
||||
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(cancellationToken);
|
||||
await using var cmd = new NpgsqlCommand(PurgeSql, connection);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
@@ -438,7 +439,7 @@ public static class VexLensTestData
|
||||
@context = "https://openvex.dev/ns/v0.2.0",
|
||||
@id = $"urn:uuid:{Guid.NewGuid()}",
|
||||
author = new { @id = "test-vendor", name = "Test Vendor" },
|
||||
timestamp = DateTimeOffset.UtcNow.ToString("O"),
|
||||
timestamp = DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture),
|
||||
statements = new[]
|
||||
{
|
||||
new
|
||||
|
||||
@@ -9,10 +9,6 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" >
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" >
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
||||
@@ -14,11 +14,6 @@
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" />
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="xunit.v3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
||||
Reference in New Issue
Block a user