sprints enhancements

This commit is contained in:
StellaOps Bot
2025-12-25 19:52:30 +02:00
parent ef6ac36323
commit b8b2d83f4a
138 changed files with 25133 additions and 594 deletions

View File

@@ -8,6 +8,7 @@ using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using StellaOps.Concelier.Core;
using StellaOps.Concelier.Core.Events;
using StellaOps.Concelier.Merge.Identity;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Storage.Advisories;
using StellaOps.Concelier.Storage.Aliases;
@@ -41,6 +42,7 @@ public sealed class AdvisoryMergeService
private readonly IAdvisoryEventLog _eventLog;
private readonly TimeProvider _timeProvider;
private readonly CanonicalMerger _canonicalMerger;
private readonly IMergeHashCalculator? _mergeHashCalculator;
private readonly ILogger<AdvisoryMergeService> _logger;
public AdvisoryMergeService(
@@ -51,7 +53,8 @@ public sealed class AdvisoryMergeService
CanonicalMerger canonicalMerger,
IAdvisoryEventLog eventLog,
TimeProvider timeProvider,
ILogger<AdvisoryMergeService> logger)
ILogger<AdvisoryMergeService> logger,
IMergeHashCalculator? mergeHashCalculator = null)
{
_aliasResolver = aliasResolver ?? throw new ArgumentNullException(nameof(aliasResolver));
_advisoryStore = advisoryStore ?? throw new ArgumentNullException(nameof(advisoryStore));
@@ -61,6 +64,7 @@ public sealed class AdvisoryMergeService
_eventLog = eventLog ?? throw new ArgumentNullException(nameof(eventLog));
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_mergeHashCalculator = mergeHashCalculator; // Optional during migration
}
public async Task<AdvisoryMergeResult> MergeAsync(string seedAdvisoryKey, CancellationToken cancellationToken)
@@ -102,7 +106,7 @@ public sealed class AdvisoryMergeService
throw;
}
var merged = precedenceResult.Advisory;
var merged = EnrichWithMergeHash(precedenceResult.Advisory);
var conflictDetails = precedenceResult.Conflicts;
if (component.Collisions.Count > 0)
@@ -309,7 +313,48 @@ public sealed class AdvisoryMergeService
source.Provenance,
source.Description,
source.Cwes,
source.CanonicalMetricId);
source.CanonicalMetricId,
source.MergeHash);
/// <summary>
/// Enriches an advisory with its computed merge hash if calculator is available.
/// </summary>
private Advisory EnrichWithMergeHash(Advisory advisory)
{
if (_mergeHashCalculator is null)
{
return advisory;
}
try
{
var mergeHash = _mergeHashCalculator.ComputeMergeHash(advisory);
return new Advisory(
advisory.AdvisoryKey,
advisory.Title,
advisory.Summary,
advisory.Language,
advisory.Published,
advisory.Modified,
advisory.Severity,
advisory.ExploitKnown,
advisory.Aliases,
advisory.Credits,
advisory.References,
advisory.AffectedPackages,
advisory.CvssMetrics,
advisory.Provenance,
advisory.Description,
advisory.Cwes,
advisory.CanonicalMetricId,
mergeHash);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to compute merge hash for {AdvisoryKey}, continuing without hash", advisory.AdvisoryKey);
return advisory;
}
}
private CanonicalMergeResult? ApplyCanonicalMergeIfNeeded(string canonicalKey, List<Advisory> inputs)
{

View File

@@ -0,0 +1,172 @@
// -----------------------------------------------------------------------------
// MergeHashBackfillService.cs
// Sprint: SPRINT_8200_0012_0001_CONCEL_merge_hash_library
// Task: MHASH-8200-020
// Description: Shadow-write mode for computing merge_hash on existing advisories
// -----------------------------------------------------------------------------
using System.Diagnostics;
using Microsoft.Extensions.Logging;
using StellaOps.Concelier.Merge.Identity;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Storage.Advisories;
namespace StellaOps.Concelier.Merge.Services;
/// <summary>
/// Service for backfilling merge hashes on existing advisories without changing their identity.
/// Runs in shadow-write mode: computes merge_hash and updates only that field.
/// </summary>
public sealed class MergeHashBackfillService
{
private readonly IAdvisoryStore _advisoryStore;
private readonly IMergeHashCalculator _mergeHashCalculator;
private readonly ILogger<MergeHashBackfillService> _logger;
public MergeHashBackfillService(
IAdvisoryStore advisoryStore,
IMergeHashCalculator mergeHashCalculator,
ILogger<MergeHashBackfillService> logger)
{
_advisoryStore = advisoryStore ?? throw new ArgumentNullException(nameof(advisoryStore));
_mergeHashCalculator = mergeHashCalculator ?? throw new ArgumentNullException(nameof(mergeHashCalculator));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>
/// Backfills merge hashes for all advisories that don't have one.
/// </summary>
/// <param name="batchSize">Number of advisories to process before yielding progress.</param>
/// <param name="dryRun">If true, computes hashes but doesn't persist them.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Backfill result with statistics.</returns>
public async Task<MergeHashBackfillResult> BackfillAsync(
int batchSize = 100,
bool dryRun = false,
CancellationToken cancellationToken = default)
{
var stopwatch = Stopwatch.StartNew();
var processed = 0;
var updated = 0;
var skipped = 0;
var errors = 0;
_logger.LogInformation(
"Starting merge hash backfill (dryRun={DryRun}, batchSize={BatchSize})",
dryRun, batchSize);
await foreach (var advisory in _advisoryStore.StreamAsync(cancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
processed++;
// Skip if already has merge hash
if (!string.IsNullOrEmpty(advisory.MergeHash))
{
skipped++;
continue;
}
try
{
var mergeHash = _mergeHashCalculator.ComputeMergeHash(advisory);
if (!dryRun)
{
var enrichedAdvisory = CreateAdvisoryWithMergeHash(advisory, mergeHash);
await _advisoryStore.UpsertAsync(enrichedAdvisory, cancellationToken).ConfigureAwait(false);
}
updated++;
if (updated % batchSize == 0)
{
_logger.LogInformation(
"Backfill progress: {Updated} updated, {Skipped} skipped, {Errors} errors (of {Processed} processed)",
updated, skipped, errors, processed);
}
}
catch (Exception ex)
{
errors++;
_logger.LogWarning(
ex,
"Failed to compute/update merge hash for {AdvisoryKey}",
advisory.AdvisoryKey);
}
}
stopwatch.Stop();
var result = new MergeHashBackfillResult(
TotalProcessed: processed,
Updated: updated,
Skipped: skipped,
Errors: errors,
DryRun: dryRun,
Duration: stopwatch.Elapsed);
_logger.LogInformation(
"Merge hash backfill completed: {Updated} updated, {Skipped} skipped, {Errors} errors (of {Processed} processed) in {Duration}",
result.Updated, result.Skipped, result.Errors, result.TotalProcessed, result.Duration);
return result;
}
/// <summary>
/// Computes merge hash for a single advisory without persisting.
/// Useful for testing or preview mode.
/// </summary>
public string ComputeMergeHash(Advisory advisory)
{
ArgumentNullException.ThrowIfNull(advisory);
return _mergeHashCalculator.ComputeMergeHash(advisory);
}
private static Advisory CreateAdvisoryWithMergeHash(Advisory source, string mergeHash)
=> new(
source.AdvisoryKey,
source.Title,
source.Summary,
source.Language,
source.Published,
source.Modified,
source.Severity,
source.ExploitKnown,
source.Aliases,
source.Credits,
source.References,
source.AffectedPackages,
source.CvssMetrics,
source.Provenance,
source.Description,
source.Cwes,
source.CanonicalMetricId,
mergeHash);
}
/// <summary>
/// Result of a merge hash backfill operation.
/// </summary>
public sealed record MergeHashBackfillResult(
int TotalProcessed,
int Updated,
int Skipped,
int Errors,
bool DryRun,
TimeSpan Duration)
{
/// <summary>
/// Percentage of advisories that were successfully updated.
/// </summary>
public double SuccessRate => TotalProcessed > 0
? (double)(Updated + Skipped) / TotalProcessed * 100
: 100;
/// <summary>
/// Average time per advisory in milliseconds.
/// </summary>
public double AvgTimePerAdvisoryMs => TotalProcessed > 0
? Duration.TotalMilliseconds / TotalProcessed
: 0;
}