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:
@@ -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; }
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user