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:
master
2025-12-29 19:12:38 +02:00
parent 41552d26ec
commit a4badc275e
286 changed files with 50918 additions and 992 deletions

View File

@@ -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";
}

View File

@@ -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);
}
}

View File

@@ -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);