UI work to fill SBOM sourcing management gap. UI planning remaining functionality exposure. Work on CI/Tests stabilization
Introduces CGS determinism test runs to CI workflows for Windows, macOS, Linux, Alpine, and Debian, fulfilling CGS-008 cross-platform requirements. Updates local-ci scripts to support new smoke steps, test timeouts, progress intervals, and project slicing for improved test isolation and diagnostics.
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// VexLensDataSource.cs
|
||||
// Sprint: SPRINT_20251229_001_002_BE_vex_delta (VEX-006)
|
||||
// Task: Create VexLens data source wrapper
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Infrastructure.Postgres.Connections;
|
||||
using StellaOps.Infrastructure.Postgres.Options;
|
||||
|
||||
namespace StellaOps.VexLens.Persistence.Postgres;
|
||||
|
||||
/// <summary>
|
||||
/// Data source for VexLens PostgreSQL connections.
|
||||
/// </summary>
|
||||
public sealed class VexLensDataSource : DataSourceBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Default schema name for VexLens tables.
|
||||
/// </summary>
|
||||
public const string DefaultSchemaName = "vex";
|
||||
|
||||
public VexLensDataSource(
|
||||
IOptions<PostgresOptions> options,
|
||||
ILogger<VexLensDataSource> logger)
|
||||
: base(options.Value, logger)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string ModuleName => "VexLens";
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// ConsensusProjectionRepository.cs
|
||||
// Sprint: SPRINT_20251229_001_002_BE_vex_delta (VEX-006)
|
||||
// Task: Implement IConsensusProjectionRepository
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
using StellaOps.VexLens.Persistence.Postgres;
|
||||
|
||||
namespace StellaOps.VexLens.Persistence.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL implementation of consensus projection repository.
|
||||
/// </summary>
|
||||
public sealed class ConsensusProjectionRepository : RepositoryBase<VexLensDataSource>, IConsensusProjectionRepository
|
||||
{
|
||||
private const string Schema = "vex";
|
||||
private const string Table = "consensus_projections";
|
||||
private const string FullTable = $"{Schema}.{Table}";
|
||||
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
|
||||
|
||||
public ConsensusProjectionRepository(
|
||||
VexLensDataSource dataSource,
|
||||
ILogger<ConsensusProjectionRepository> logger)
|
||||
: base(dataSource, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public async ValueTask<ConsensusProjection> AddAsync(
|
||||
ConsensusProjection projection,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
const string sql = $"""
|
||||
INSERT INTO {FullTable} (
|
||||
id, tenant_id, vulnerability_id, product_key, status,
|
||||
confidence_score, outcome, statement_count, conflict_count,
|
||||
merge_trace, computed_at, previous_projection_id, status_changed
|
||||
)
|
||||
VALUES (
|
||||
@id, @tenantId, @vulnId, @productKey, @status,
|
||||
@confidence, @outcome, @stmtCount, @conflictCount,
|
||||
@mergeTrace::jsonb, @computedAt, @previousId, @statusChanged
|
||||
)
|
||||
RETURNING id, tenant_id, vulnerability_id, product_key, status,
|
||||
confidence_score, outcome, statement_count, conflict_count,
|
||||
merge_trace, computed_at, stored_at, previous_projection_id, status_changed
|
||||
""";
|
||||
|
||||
var result = await QuerySingleOrDefaultAsync(
|
||||
projection.TenantId.ToString(),
|
||||
sql,
|
||||
cmd =>
|
||||
{
|
||||
AddParameter(cmd, "id", projection.Id);
|
||||
AddParameter(cmd, "tenantId", projection.TenantId);
|
||||
AddParameter(cmd, "vulnId", projection.VulnerabilityId);
|
||||
AddParameter(cmd, "productKey", projection.ProductKey);
|
||||
AddParameter(cmd, "status", projection.Status.ToString().ToLowerInvariant());
|
||||
AddParameter(cmd, "confidence", projection.ConfidenceScore);
|
||||
AddParameter(cmd, "outcome", projection.Outcome);
|
||||
AddParameter(cmd, "stmtCount", projection.StatementCount);
|
||||
AddParameter(cmd, "conflictCount", projection.ConflictCount);
|
||||
AddParameter(cmd, "mergeTrace", SerializeTrace(projection.Trace));
|
||||
AddParameter(cmd, "computedAt", projection.ComputedAt);
|
||||
AddParameter(cmd, "previousId", (object?)projection.PreviousProjectionId ?? DBNull.Value);
|
||||
AddParameter(cmd, "statusChanged", projection.StatusChanged);
|
||||
},
|
||||
MapProjection,
|
||||
ct);
|
||||
|
||||
return result ?? throw new InvalidOperationException("Failed to add consensus projection");
|
||||
}
|
||||
|
||||
public async ValueTask<ConsensusProjection?> GetLatestAsync(
|
||||
string vulnerabilityId,
|
||||
string productKey,
|
||||
Guid tenantId,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
const string sql = $"""
|
||||
SELECT id, tenant_id, vulnerability_id, product_key, status,
|
||||
confidence_score, outcome, statement_count, conflict_count,
|
||||
merge_trace, computed_at, stored_at, previous_projection_id, status_changed
|
||||
FROM {FullTable}
|
||||
WHERE vulnerability_id = @vulnId
|
||||
AND product_key = @productKey
|
||||
AND tenant_id = @tenantId
|
||||
ORDER BY computed_at DESC
|
||||
LIMIT 1
|
||||
""";
|
||||
|
||||
return await QuerySingleOrDefaultAsync(
|
||||
tenantId.ToString(),
|
||||
sql,
|
||||
cmd =>
|
||||
{
|
||||
AddParameter(cmd, "vulnId", vulnerabilityId);
|
||||
AddParameter(cmd, "productKey", productKey);
|
||||
AddParameter(cmd, "tenantId", tenantId);
|
||||
},
|
||||
MapProjection,
|
||||
ct);
|
||||
}
|
||||
|
||||
public async ValueTask<IReadOnlyList<ConsensusProjection>> GetByVulnerabilityAsync(
|
||||
string vulnerabilityId,
|
||||
Guid tenantId,
|
||||
int limit = 100,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var sql = $"""
|
||||
SELECT id, tenant_id, vulnerability_id, product_key, status,
|
||||
confidence_score, outcome, statement_count, conflict_count,
|
||||
merge_trace, computed_at, stored_at, previous_projection_id, status_changed
|
||||
FROM {FullTable}
|
||||
WHERE vulnerability_id = @vulnId
|
||||
AND tenant_id = @tenantId
|
||||
ORDER BY computed_at DESC
|
||||
LIMIT @limit
|
||||
""";
|
||||
|
||||
return await QueryAsync(
|
||||
tenantId.ToString(),
|
||||
sql,
|
||||
cmd =>
|
||||
{
|
||||
AddParameter(cmd, "vulnId", vulnerabilityId);
|
||||
AddParameter(cmd, "tenantId", tenantId);
|
||||
AddParameter(cmd, "limit", limit);
|
||||
},
|
||||
MapProjection,
|
||||
ct);
|
||||
}
|
||||
|
||||
public async ValueTask<IReadOnlyList<ConsensusProjection>> GetByProductAsync(
|
||||
string productKey,
|
||||
Guid tenantId,
|
||||
int limit = 100,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var sql = $"""
|
||||
SELECT id, tenant_id, vulnerability_id, product_key, status,
|
||||
confidence_score, outcome, statement_count, conflict_count,
|
||||
merge_trace, computed_at, stored_at, previous_projection_id, status_changed
|
||||
FROM {FullTable}
|
||||
WHERE product_key = @productKey
|
||||
AND tenant_id = @tenantId
|
||||
ORDER BY computed_at DESC
|
||||
LIMIT @limit
|
||||
""";
|
||||
|
||||
return await QueryAsync(
|
||||
tenantId.ToString(),
|
||||
sql,
|
||||
cmd =>
|
||||
{
|
||||
AddParameter(cmd, "productKey", productKey);
|
||||
AddParameter(cmd, "tenantId", tenantId);
|
||||
AddParameter(cmd, "limit", limit);
|
||||
},
|
||||
MapProjection,
|
||||
ct);
|
||||
}
|
||||
|
||||
public async ValueTask<IReadOnlyList<ConsensusProjection>> GetStatusChangesAsync(
|
||||
Guid tenantId,
|
||||
DateTimeOffset since,
|
||||
int limit = 100,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var sql = $"""
|
||||
SELECT id, tenant_id, vulnerability_id, product_key, status,
|
||||
confidence_score, outcome, statement_count, conflict_count,
|
||||
merge_trace, computed_at, stored_at, previous_projection_id, status_changed
|
||||
FROM {FullTable}
|
||||
WHERE tenant_id = @tenantId
|
||||
AND status_changed = TRUE
|
||||
AND computed_at >= @since
|
||||
ORDER BY computed_at DESC
|
||||
LIMIT @limit
|
||||
""";
|
||||
|
||||
return await QueryAsync(
|
||||
tenantId.ToString(),
|
||||
sql,
|
||||
cmd =>
|
||||
{
|
||||
AddParameter(cmd, "tenantId", tenantId);
|
||||
AddParameter(cmd, "since", since);
|
||||
AddParameter(cmd, "limit", limit);
|
||||
},
|
||||
MapProjection,
|
||||
ct);
|
||||
}
|
||||
|
||||
public async ValueTask<IReadOnlyList<ConsensusProjection>> GetHistoryAsync(
|
||||
string vulnerabilityId,
|
||||
string productKey,
|
||||
Guid tenantId,
|
||||
int limit = 50,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var sql = $"""
|
||||
SELECT id, tenant_id, vulnerability_id, product_key, status,
|
||||
confidence_score, outcome, statement_count, conflict_count,
|
||||
merge_trace, computed_at, stored_at, previous_projection_id, status_changed
|
||||
FROM {FullTable}
|
||||
WHERE vulnerability_id = @vulnId
|
||||
AND product_key = @productKey
|
||||
AND tenant_id = @tenantId
|
||||
ORDER BY computed_at DESC
|
||||
LIMIT @limit
|
||||
""";
|
||||
|
||||
return await QueryAsync(
|
||||
tenantId.ToString(),
|
||||
sql,
|
||||
cmd =>
|
||||
{
|
||||
AddParameter(cmd, "vulnId", vulnerabilityId);
|
||||
AddParameter(cmd, "productKey", productKey);
|
||||
AddParameter(cmd, "tenantId", tenantId);
|
||||
AddParameter(cmd, "limit", limit);
|
||||
},
|
||||
MapProjection,
|
||||
ct);
|
||||
}
|
||||
|
||||
private static ConsensusProjection MapProjection(System.Data.Common.DbDataReader reader)
|
||||
{
|
||||
var statusStr = reader.GetString(reader.GetOrdinal("status"));
|
||||
var status = statusStr.ToLowerInvariant() switch
|
||||
{
|
||||
"unknown" => VexConsensusStatus.Unknown,
|
||||
"under_investigation" => VexConsensusStatus.UnderInvestigation,
|
||||
"not_affected" => VexConsensusStatus.NotAffected,
|
||||
"affected" => VexConsensusStatus.Affected,
|
||||
"fixed" => VexConsensusStatus.Fixed,
|
||||
_ => throw new InvalidOperationException($"Unknown status: {statusStr}")
|
||||
};
|
||||
|
||||
var traceJson = reader.IsDBNull(reader.GetOrdinal("merge_trace"))
|
||||
? null
|
||||
: reader.GetString(reader.GetOrdinal("merge_trace"));
|
||||
|
||||
return new ConsensusProjection(
|
||||
Id: reader.GetGuid(reader.GetOrdinal("id")),
|
||||
TenantId: reader.GetGuid(reader.GetOrdinal("tenant_id")),
|
||||
VulnerabilityId: reader.GetString(reader.GetOrdinal("vulnerability_id")),
|
||||
ProductKey: reader.GetString(reader.GetOrdinal("product_key")),
|
||||
Status: status,
|
||||
ConfidenceScore: reader.GetDecimal(reader.GetOrdinal("confidence_score")),
|
||||
Outcome: reader.GetString(reader.GetOrdinal("outcome")),
|
||||
StatementCount: reader.GetInt32(reader.GetOrdinal("statement_count")),
|
||||
ConflictCount: reader.GetInt32(reader.GetOrdinal("conflict_count")),
|
||||
Trace: DeserializeTrace(traceJson),
|
||||
ComputedAt: reader.GetFieldValue<DateTimeOffset>(reader.GetOrdinal("computed_at")),
|
||||
StoredAt: reader.GetFieldValue<DateTimeOffset>(reader.GetOrdinal("stored_at")),
|
||||
PreviousProjectionId: reader.IsDBNull(reader.GetOrdinal("previous_projection_id"))
|
||||
? null
|
||||
: reader.GetGuid(reader.GetOrdinal("previous_projection_id")),
|
||||
StatusChanged: reader.GetBoolean(reader.GetOrdinal("status_changed"))
|
||||
);
|
||||
}
|
||||
|
||||
private static string SerializeTrace(MergeTrace? trace)
|
||||
{
|
||||
if (trace == null)
|
||||
return "{}";
|
||||
|
||||
return JsonSerializer.Serialize(trace, SerializerOptions);
|
||||
}
|
||||
|
||||
private static MergeTrace? DeserializeTrace(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json) || json == "{}")
|
||||
return null;
|
||||
|
||||
return JsonSerializer.Deserialize<MergeTrace>(json, SerializerOptions);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// IConsensusProjectionRepository.cs
|
||||
// Sprint: SPRINT_20251229_001_002_BE_vex_delta (VEX-006)
|
||||
// Task: Implement IConsensusProjectionRepository
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
namespace StellaOps.VexLens.Persistence.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository for VEX consensus projections.
|
||||
/// Replaces in-memory VexLens store with PostgreSQL persistence.
|
||||
/// </summary>
|
||||
public interface IConsensusProjectionRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Add a new consensus projection.
|
||||
/// </summary>
|
||||
ValueTask<ConsensusProjection> AddAsync(
|
||||
ConsensusProjection projection,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get the latest consensus projection for a vulnerability/product combination.
|
||||
/// </summary>
|
||||
ValueTask<ConsensusProjection?> GetLatestAsync(
|
||||
string vulnerabilityId,
|
||||
string productKey,
|
||||
Guid tenantId,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get all projections for a vulnerability across all products.
|
||||
/// </summary>
|
||||
ValueTask<IReadOnlyList<ConsensusProjection>> GetByVulnerabilityAsync(
|
||||
string vulnerabilityId,
|
||||
Guid tenantId,
|
||||
int limit = 100,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get all projections for a product across all vulnerabilities.
|
||||
/// </summary>
|
||||
ValueTask<IReadOnlyList<ConsensusProjection>> GetByProductAsync(
|
||||
string productKey,
|
||||
Guid tenantId,
|
||||
int limit = 100,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get projections where status changed from previous.
|
||||
/// Useful for identifying new/resolved vulnerabilities.
|
||||
/// </summary>
|
||||
ValueTask<IReadOnlyList<ConsensusProjection>> GetStatusChangesAsync(
|
||||
Guid tenantId,
|
||||
DateTimeOffset since,
|
||||
int limit = 100,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get projection history for a vulnerability/product pair.
|
||||
/// </summary>
|
||||
ValueTask<IReadOnlyList<ConsensusProjection>> GetHistoryAsync(
|
||||
string vulnerabilityId,
|
||||
string productKey,
|
||||
Guid tenantId,
|
||||
int limit = 50,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// VEX consensus projection record.
|
||||
/// </summary>
|
||||
public sealed record ConsensusProjection(
|
||||
Guid Id,
|
||||
Guid TenantId,
|
||||
string VulnerabilityId,
|
||||
string ProductKey,
|
||||
VexConsensusStatus Status,
|
||||
decimal ConfidenceScore,
|
||||
string Outcome,
|
||||
int StatementCount,
|
||||
int ConflictCount,
|
||||
MergeTrace? Trace,
|
||||
DateTimeOffset ComputedAt,
|
||||
DateTimeOffset StoredAt,
|
||||
Guid? PreviousProjectionId,
|
||||
bool StatusChanged);
|
||||
|
||||
/// <summary>
|
||||
/// VEX consensus status values.
|
||||
/// </summary>
|
||||
public enum VexConsensusStatus
|
||||
{
|
||||
Unknown,
|
||||
UnderInvestigation,
|
||||
NotAffected,
|
||||
Affected,
|
||||
Fixed
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merge trace showing how consensus was computed.
|
||||
/// </summary>
|
||||
public sealed record MergeTrace(
|
||||
IReadOnlyList<MergeTraceStep> Steps,
|
||||
string Algorithm,
|
||||
IReadOnlyDictionary<string, object>? Metadata);
|
||||
|
||||
/// <summary>
|
||||
/// Single step in merge trace.
|
||||
/// </summary>
|
||||
public sealed record MergeTraceStep(
|
||||
int Order,
|
||||
string Action,
|
||||
string Source,
|
||||
object Input,
|
||||
object Output);
|
||||
Reference in New Issue
Block a user