sprints work

This commit is contained in:
master
2026-01-11 11:19:40 +02:00
parent f6ef1ef337
commit 582a41d7a9
72 changed files with 2680 additions and 390 deletions

View File

@@ -0,0 +1,218 @@
// Licensed under AGPL-3.0-or-later. Copyright (C) 2026 StellaOps Contributors.
// Sprint: SPRINT_20260110_012_004_BINDEX
// Task: GSD-007 - IDiffResultStore Interface
using System.Collections.Immutable;
namespace StellaOps.BinaryIndex.Diff;
/// <summary>
/// Storage interface for patch diff results.
/// Provides persistence and caching for verification results.
/// </summary>
public interface IDiffResultStore
{
/// <summary>
/// Stores a patch diff result.
/// </summary>
/// <param name="result">The diff result to store.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Unique ID of the stored result.</returns>
Task<Guid> StoreAsync(PatchDiffResult result, CancellationToken ct = default);
/// <summary>
/// Retrieves a diff result by ID.
/// </summary>
/// <param name="id">The result ID.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The diff result, or null if not found.</returns>
Task<PatchDiffResult?> GetByIdAsync(Guid id, CancellationToken ct = default);
/// <summary>
/// Finds diff results for a specific binary pair.
/// </summary>
/// <param name="preBinaryDigest">Pre-patch binary digest.</param>
/// <param name="postBinaryDigest">Post-patch binary digest.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>List of matching diff results.</returns>
Task<ImmutableArray<StoredDiffResult>> FindByBinariesAsync(
string preBinaryDigest,
string postBinaryDigest,
CancellationToken ct = default);
/// <summary>
/// Finds diff results for a specific golden set.
/// </summary>
/// <param name="goldenSetId">Golden set ID.</param>
/// <param name="limit">Maximum results to return.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>List of diff results for the golden set.</returns>
Task<ImmutableArray<StoredDiffResult>> FindByGoldenSetAsync(
string goldenSetId,
int limit = 100,
CancellationToken ct = default);
/// <summary>
/// Gets a cached single binary check result.
/// </summary>
/// <param name="binaryDigest">Binary digest.</param>
/// <param name="vulnerabilityId">Vulnerability ID (golden set ID).</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Cached result, or null if not cached.</returns>
Task<SingleBinaryCheckResult?> GetCachedCheckAsync(
string binaryDigest,
string vulnerabilityId,
CancellationToken ct = default);
/// <summary>
/// Caches a single binary check result.
/// </summary>
/// <param name="binaryDigest">Binary digest.</param>
/// <param name="vulnerabilityId">Vulnerability ID (golden set ID).</param>
/// <param name="result">Check result to cache.</param>
/// <param name="ttl">Time-to-live for the cache entry.</param>
/// <param name="ct">Cancellation token.</param>
Task CacheCheckAsync(
string binaryDigest,
string vulnerabilityId,
SingleBinaryCheckResult result,
TimeSpan? ttl = null,
CancellationToken ct = default);
/// <summary>
/// Queries stored diff results.
/// </summary>
/// <param name="query">Query parameters.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Matching diff results.</returns>
Task<DiffResultQueryResponse> QueryAsync(
DiffResultQuery query,
CancellationToken ct = default);
/// <summary>
/// Gets statistics about stored diff results.
/// </summary>
/// <param name="ct">Cancellation token.</param>
/// <returns>Storage statistics.</returns>
Task<DiffResultStoreStats> GetStatsAsync(CancellationToken ct = default);
}
/// <summary>
/// Stored diff result with metadata.
/// </summary>
public sealed record StoredDiffResult
{
/// <summary>Unique ID.</summary>
public required Guid Id { get; init; }
/// <summary>The diff result.</summary>
public required PatchDiffResult Result { get; init; }
/// <summary>When the result was stored.</summary>
public required DateTimeOffset StoredAt { get; init; }
}
/// <summary>
/// Query parameters for searching diff results.
/// </summary>
public sealed record DiffResultQuery
{
/// <summary>Filter by golden set ID.</summary>
public string? GoldenSetId { get; init; }
/// <summary>Filter by verdict.</summary>
public PatchVerdict? Verdict { get; init; }
/// <summary>Filter by minimum confidence.</summary>
public decimal? MinConfidence { get; init; }
/// <summary>Filter by pre-binary digest.</summary>
public string? PreBinaryDigest { get; init; }
/// <summary>Filter by post-binary digest.</summary>
public string? PostBinaryDigest { get; init; }
/// <summary>Filter by comparison date (after).</summary>
public DateTimeOffset? ComparedAfter { get; init; }
/// <summary>Filter by comparison date (before).</summary>
public DateTimeOffset? ComparedBefore { get; init; }
/// <summary>Maximum results to return.</summary>
public int Limit { get; init; } = 100;
/// <summary>Pagination offset.</summary>
public int Offset { get; init; } = 0;
/// <summary>Order by field.</summary>
public DiffResultOrderBy OrderBy { get; init; } = DiffResultOrderBy.ComparedAtDesc;
}
/// <summary>
/// Response from a diff result query.
/// </summary>
public sealed record DiffResultQueryResponse
{
/// <summary>Matching results.</summary>
public required ImmutableArray<StoredDiffResult> Results { get; init; }
/// <summary>Total count of matching results (for pagination).</summary>
public required int TotalCount { get; init; }
/// <summary>Query offset.</summary>
public required int Offset { get; init; }
/// <summary>Query limit.</summary>
public required int Limit { get; init; }
}
/// <summary>
/// Ordering options for diff result queries.
/// </summary>
public enum DiffResultOrderBy
{
/// <summary>Order by comparison date ascending.</summary>
ComparedAtAsc,
/// <summary>Order by comparison date descending.</summary>
ComparedAtDesc,
/// <summary>Order by confidence ascending.</summary>
ConfidenceAsc,
/// <summary>Order by confidence descending.</summary>
ConfidenceDesc,
/// <summary>Order by golden set ID ascending.</summary>
GoldenSetIdAsc,
/// <summary>Order by golden set ID descending.</summary>
GoldenSetIdDesc
}
/// <summary>
/// Statistics about the diff result store.
/// </summary>
public sealed record DiffResultStoreStats
{
/// <summary>Total number of stored results.</summary>
public required long TotalResults { get; init; }
/// <summary>Results by verdict.</summary>
public required ImmutableDictionary<PatchVerdict, long> ResultsByVerdict { get; init; }
/// <summary>Number of unique golden sets.</summary>
public required int UniqueGoldenSets { get; init; }
/// <summary>Number of unique binary pairs.</summary>
public required long UniqueBinaryPairs { get; init; }
/// <summary>Number of cached check results.</summary>
public required long CachedChecks { get; init; }
/// <summary>Oldest result timestamp.</summary>
public DateTimeOffset? OldestResult { get; init; }
/// <summary>Newest result timestamp.</summary>
public DateTimeOffset? NewestResult { get; init; }
}

View File

@@ -0,0 +1,245 @@
// Licensed under AGPL-3.0-or-later. Copyright (C) 2026 StellaOps Contributors.
// Sprint: SPRINT_20260110_012_004_BINDEX
// Task: GSD-007 - IDiffResultStore Interface - InMemory Implementation
using System.Collections.Concurrent;
using System.Collections.Immutable;
namespace StellaOps.BinaryIndex.Diff;
/// <summary>
/// In-memory implementation of <see cref="IDiffResultStore"/> for testing.
/// </summary>
public sealed class InMemoryDiffResultStore : IDiffResultStore
{
private readonly ConcurrentDictionary<Guid, StoredDiffResult> _results = new();
private readonly ConcurrentDictionary<string, SingleBinaryCheckResult> _checkCache = new();
private readonly TimeProvider _timeProvider;
public InMemoryDiffResultStore(TimeProvider? timeProvider = null)
{
_timeProvider = timeProvider ?? TimeProvider.System;
}
/// <inheritdoc />
public Task<Guid> StoreAsync(PatchDiffResult result, CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(result);
ct.ThrowIfCancellationRequested();
var id = Guid.NewGuid();
var stored = new StoredDiffResult
{
Id = id,
Result = result,
StoredAt = _timeProvider.GetUtcNow()
};
_results[id] = stored;
return Task.FromResult(id);
}
/// <inheritdoc />
public Task<PatchDiffResult?> GetByIdAsync(Guid id, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
return Task.FromResult(_results.TryGetValue(id, out var stored) ? stored.Result : null);
}
/// <inheritdoc />
public Task<ImmutableArray<StoredDiffResult>> FindByBinariesAsync(
string preBinaryDigest,
string postBinaryDigest,
CancellationToken ct = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(preBinaryDigest);
ArgumentException.ThrowIfNullOrWhiteSpace(postBinaryDigest);
ct.ThrowIfCancellationRequested();
var matches = _results.Values
.Where(s => string.Equals(s.Result.PreBinaryDigest, preBinaryDigest, StringComparison.OrdinalIgnoreCase)
&& string.Equals(s.Result.PostBinaryDigest, postBinaryDigest, StringComparison.OrdinalIgnoreCase))
.OrderByDescending(s => s.StoredAt)
.ToImmutableArray();
return Task.FromResult(matches);
}
/// <inheritdoc />
public Task<ImmutableArray<StoredDiffResult>> FindByGoldenSetAsync(
string goldenSetId,
int limit = 100,
CancellationToken ct = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(goldenSetId);
ct.ThrowIfCancellationRequested();
var matches = _results.Values
.Where(s => string.Equals(s.Result.GoldenSetId, goldenSetId, StringComparison.OrdinalIgnoreCase))
.OrderByDescending(s => s.StoredAt)
.Take(limit)
.ToImmutableArray();
return Task.FromResult(matches);
}
/// <inheritdoc />
public Task<SingleBinaryCheckResult?> GetCachedCheckAsync(
string binaryDigest,
string vulnerabilityId,
CancellationToken ct = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(binaryDigest);
ArgumentException.ThrowIfNullOrWhiteSpace(vulnerabilityId);
ct.ThrowIfCancellationRequested();
var key = GetCacheKey(binaryDigest, vulnerabilityId);
return Task.FromResult(_checkCache.TryGetValue(key, out var result) ? result : null);
}
/// <inheritdoc />
public Task CacheCheckAsync(
string binaryDigest,
string vulnerabilityId,
SingleBinaryCheckResult result,
TimeSpan? ttl = null,
CancellationToken ct = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(binaryDigest);
ArgumentException.ThrowIfNullOrWhiteSpace(vulnerabilityId);
ArgumentNullException.ThrowIfNull(result);
ct.ThrowIfCancellationRequested();
var key = GetCacheKey(binaryDigest, vulnerabilityId);
_checkCache[key] = result;
// Note: TTL not implemented for in-memory store (testing only)
return Task.CompletedTask;
}
/// <inheritdoc />
public Task<DiffResultQueryResponse> QueryAsync(
DiffResultQuery query,
CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(query);
ct.ThrowIfCancellationRequested();
IEnumerable<StoredDiffResult> results = _results.Values;
// Apply filters
if (!string.IsNullOrWhiteSpace(query.GoldenSetId))
{
results = results.Where(r =>
string.Equals(r.Result.GoldenSetId, query.GoldenSetId, StringComparison.OrdinalIgnoreCase));
}
if (query.Verdict.HasValue)
{
results = results.Where(r => r.Result.Verdict == query.Verdict.Value);
}
if (query.MinConfidence.HasValue)
{
results = results.Where(r => r.Result.Confidence >= query.MinConfidence.Value);
}
if (!string.IsNullOrWhiteSpace(query.PreBinaryDigest))
{
results = results.Where(r =>
string.Equals(r.Result.PreBinaryDigest, query.PreBinaryDigest, StringComparison.OrdinalIgnoreCase));
}
if (!string.IsNullOrWhiteSpace(query.PostBinaryDigest))
{
results = results.Where(r =>
string.Equals(r.Result.PostBinaryDigest, query.PostBinaryDigest, StringComparison.OrdinalIgnoreCase));
}
if (query.ComparedAfter.HasValue)
{
results = results.Where(r => r.Result.Metadata.ComparedAt >= query.ComparedAfter.Value);
}
if (query.ComparedBefore.HasValue)
{
results = results.Where(r => r.Result.Metadata.ComparedAt <= query.ComparedBefore.Value);
}
// Apply ordering
results = query.OrderBy switch
{
DiffResultOrderBy.ComparedAtAsc => results.OrderBy(r => r.Result.Metadata.ComparedAt),
DiffResultOrderBy.ComparedAtDesc => results.OrderByDescending(r => r.Result.Metadata.ComparedAt),
DiffResultOrderBy.ConfidenceAsc => results.OrderBy(r => r.Result.Confidence),
DiffResultOrderBy.ConfidenceDesc => results.OrderByDescending(r => r.Result.Confidence),
DiffResultOrderBy.GoldenSetIdAsc => results.OrderBy(r => r.Result.GoldenSetId, StringComparer.OrdinalIgnoreCase),
DiffResultOrderBy.GoldenSetIdDesc => results.OrderByDescending(r => r.Result.GoldenSetId, StringComparer.OrdinalIgnoreCase),
_ => results.OrderByDescending(r => r.StoredAt)
};
var allResults = results.ToList();
var totalCount = allResults.Count;
var pagedResults = allResults
.Skip(query.Offset)
.Take(query.Limit)
.ToImmutableArray();
return Task.FromResult(new DiffResultQueryResponse
{
Results = pagedResults,
TotalCount = totalCount,
Offset = query.Offset,
Limit = query.Limit
});
}
/// <inheritdoc />
public Task<DiffResultStoreStats> GetStatsAsync(CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
var results = _results.Values.ToList();
var resultsByVerdict = results
.GroupBy(r => r.Result.Verdict)
.ToImmutableDictionary(g => g.Key, g => (long)g.Count());
var uniqueGoldenSets = results
.Select(r => r.Result.GoldenSetId)
.Distinct(StringComparer.OrdinalIgnoreCase)
.Count();
var uniqueBinaryPairs = results
.Select(r => $"{r.Result.PreBinaryDigest}:{r.Result.PostBinaryDigest}")
.Distinct(StringComparer.OrdinalIgnoreCase)
.Count();
var oldestResult = results.MinBy(r => r.StoredAt)?.StoredAt;
var newestResult = results.MaxBy(r => r.StoredAt)?.StoredAt;
return Task.FromResult(new DiffResultStoreStats
{
TotalResults = results.Count,
ResultsByVerdict = resultsByVerdict,
UniqueGoldenSets = uniqueGoldenSets,
UniqueBinaryPairs = uniqueBinaryPairs,
CachedChecks = _checkCache.Count,
OldestResult = oldestResult,
NewestResult = newestResult
});
}
/// <summary>
/// Clears all stored results and cache.
/// </summary>
public void Clear()
{
_results.Clear();
_checkCache.Clear();
}
private static string GetCacheKey(string binaryDigest, string vulnerabilityId)
=> $"{binaryDigest}:{vulnerabilityId}";
}