feat: Add VEX Status Chip component and integration tests for reachability drift detection

- Introduced `VexStatusChipComponent` to display VEX status with color coding and tooltips.
- Implemented integration tests for reachability drift detection, covering various scenarios including drift detection, determinism, and error handling.
- Enhanced `ScannerToSignalsReachabilityTests` with a null implementation of `ICallGraphSyncService` for better test isolation.
- Updated project references to include the new Reachability Drift library.
This commit is contained in:
StellaOps Bot
2025-12-20 01:26:42 +02:00
parent edc91ea96f
commit 5fc469ad98
159 changed files with 41116 additions and 2305 deletions

View File

@@ -0,0 +1,75 @@
using StellaOps.Unknowns.Core.Models;
namespace StellaOps.Unknowns.Core.Persistence;
/// <summary>
/// Abstraction for persisting unknowns from Scanner.Worker.
/// This decouples Scanner from specific storage implementations (Postgres, etc.).
/// </summary>
/// <remarks>
/// Sprint: SPRINT_3500_0013_0001 - Native Unknowns Integration
/// </remarks>
public interface IUnknownPersister
{
/// <summary>
/// Persists a single unknown.
/// </summary>
/// <param name="unknown">The unknown to persist.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The persisted unknown with ID assigned.</returns>
Task<Unknown> PersistAsync(UnknownInput unknown, CancellationToken cancellationToken = default);
/// <summary>
/// Persists a batch of unknowns.
/// </summary>
/// <param name="unknowns">The unknowns to persist.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The count of unknowns persisted (excluding duplicates).</returns>
Task<int> PersistBatchAsync(IEnumerable<UnknownInput> unknowns, CancellationToken cancellationToken = default);
/// <summary>
/// Checks if an unknown with the given subject hash already exists.
/// </summary>
/// <param name="tenantId">The tenant ID.</param>
/// <param name="subjectHash">The subject hash to check.</param>
/// <param name="kind">The kind of unknown.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>True if an open unknown with this subject hash exists.</returns>
Task<bool> ExistsAsync(string tenantId, string subjectHash, UnknownKind kind, CancellationToken cancellationToken = default);
}
/// <summary>
/// Input model for creating a new unknown via the persister.
/// </summary>
public sealed record UnknownInput
{
/// <summary>Tenant that owns this unknown.</summary>
public required string TenantId { get; init; }
/// <summary>Type of subject (package, binary, etc.).</summary>
public required UnknownSubjectType SubjectType { get; init; }
/// <summary>Human-readable reference (purl, file path, etc.).</summary>
public required string SubjectRef { get; init; }
/// <summary>Classification of the unknown.</summary>
public required UnknownKind Kind { get; init; }
/// <summary>Severity assessment (optional).</summary>
public UnknownSeverity? Severity { get; init; }
/// <summary>Additional context as JSON string.</summary>
public string? Context { get; init; }
/// <summary>ID of the scan that discovered this unknown.</summary>
public Guid? SourceScanId { get; init; }
/// <summary>ID of the call graph context.</summary>
public Guid? SourceGraphId { get; init; }
/// <summary>SBOM digest if applicable.</summary>
public string? SourceSbomDigest { get; init; }
/// <summary>Who/what created this record.</summary>
public required string CreatedBy { get; init; }
}

View File

@@ -0,0 +1,119 @@
using Microsoft.Extensions.Logging;
using StellaOps.Unknowns.Core.Models;
using StellaOps.Unknowns.Core.Persistence;
using StellaOps.Unknowns.Core.Repositories;
namespace StellaOps.Unknowns.Storage.Postgres.Persistence;
/// <summary>
/// PostgreSQL implementation of the unknown persister.
/// Wraps IUnknownRepository to provide a simpler persistence interface for Scanner.Worker.
/// </summary>
/// <remarks>
/// Sprint: SPRINT_3500_0013_0001 - Native Unknowns Integration
/// </remarks>
public sealed class PostgresUnknownPersister : IUnknownPersister
{
private readonly IUnknownRepository _repository;
private readonly ILogger<PostgresUnknownPersister> _logger;
public PostgresUnknownPersister(
IUnknownRepository repository,
ILogger<PostgresUnknownPersister> logger)
{
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <inheritdoc />
public async Task<Unknown> PersistAsync(UnknownInput input, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(input);
_logger.LogDebug(
"Persisting unknown for tenant {TenantId}, kind={Kind}, subject={SubjectRef}",
input.TenantId, input.Kind, input.SubjectRef);
var unknown = await _repository.CreateAsync(
input.TenantId,
input.SubjectType,
input.SubjectRef,
input.Kind,
input.Severity,
input.Context,
input.SourceScanId,
input.SourceGraphId,
input.SourceSbomDigest,
input.CreatedBy,
cancellationToken);
_logger.LogInformation(
"Persisted unknown {Id} for tenant {TenantId}, kind={Kind}",
unknown.Id, input.TenantId, input.Kind);
return unknown;
}
/// <inheritdoc />
public async Task<int> PersistBatchAsync(IEnumerable<UnknownInput> unknowns, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(unknowns);
var count = 0;
foreach (var input in unknowns)
{
if (cancellationToken.IsCancellationRequested)
{
break;
}
try
{
// Check for duplicates before inserting
var exists = await ExistsAsync(
input.TenantId,
ComputeSubjectHash(input.SubjectRef),
input.Kind,
cancellationToken);
if (exists)
{
_logger.LogDebug(
"Skipping duplicate unknown for {SubjectRef}, kind={Kind}",
input.SubjectRef, input.Kind);
continue;
}
await PersistAsync(input, cancellationToken);
count++;
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
_logger.LogWarning(ex,
"Failed to persist unknown for {SubjectRef}, kind={Kind}",
input.SubjectRef, input.Kind);
}
}
_logger.LogInformation("Persisted {Count} unknowns in batch", count);
return count;
}
/// <inheritdoc />
public async Task<bool> ExistsAsync(
string tenantId,
string subjectHash,
UnknownKind kind,
CancellationToken cancellationToken = default)
{
var existing = await _repository.GetBySubjectHashAsync(tenantId, subjectHash, kind, cancellationToken);
return existing is not null && existing.IsOpen;
}
private static string ComputeSubjectHash(string subjectRef)
{
var bytes = System.Security.Cryptography.SHA256.HashData(
System.Text.Encoding.UTF8.GetBytes(subjectRef));
return Convert.ToHexString(bytes).ToLowerInvariant();
}
}