blockers 2
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-11-23 14:54:17 +02:00
parent f47d2d1377
commit cce96f3596
100 changed files with 2758 additions and 1912 deletions

View File

@@ -0,0 +1,73 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
namespace StellaOps.Excititor.Core.Observations;
/// <summary>
/// Builds deterministic linkset update events from raw VEX observations
/// without introducing consensus or derived semantics (AOC-19-002).
/// </summary>
public sealed class VexLinksetExtractionService
{
/// <summary>
/// Groups observations by (vulnerabilityId, productKey) and emits a linkset update event
/// for each group. Ordering is stable and case-insensitive on identifiers.
/// </summary>
public ImmutableArray<VexLinksetUpdatedEvent> Extract(
string tenant,
IEnumerable<VexObservation> observations,
IEnumerable<VexObservationDisagreement>? disagreements = null)
{
if (observations is null)
{
return ImmutableArray<VexLinksetUpdatedEvent>.Empty;
}
var observationList = observations
.Where(o => o is not null)
.ToList();
if (observationList.Count == 0)
{
return ImmutableArray<VexLinksetUpdatedEvent>.Empty;
}
var groups = observationList
.SelectMany(obs => obs.Statements.Select(stmt => (obs, stmt)))
.GroupBy(x => new
{
VulnerabilityId = Normalize(x.stmt.VulnerabilityId),
ProductKey = Normalize(x.stmt.ProductKey)
})
.OrderBy(g => g.Key.VulnerabilityId, StringComparer.OrdinalIgnoreCase)
.ThenBy(g => g.Key.ProductKey, StringComparer.OrdinalIgnoreCase);
var now = observationList.Max(o => o.CreatedAt);
var events = new List<VexLinksetUpdatedEvent>();
foreach (var group in groups)
{
var linksetId = BuildLinksetId(group.Key.VulnerabilityId, group.Key.ProductKey);
var obsForGroup = group.Select(x => x.obs);
var evt = VexLinksetUpdatedEventFactory.Create(
tenant,
linksetId,
group.Key.VulnerabilityId,
group.Key.ProductKey,
obsForGroup,
disagreements ?? Enumerable.Empty<VexObservationDisagreement>(),
now);
events.Add(evt);
}
return events.ToImmutableArray();
}
private static string BuildLinksetId(string vulnerabilityId, string productKey)
=> $"vex:{vulnerabilityId}:{productKey}".ToLowerInvariant();
private static string Normalize(string value) => VexObservation.EnsureNotNullOrWhiteSpace(value, nameof(value));
}

View File

@@ -10,6 +10,19 @@ public interface IAirgapImportStore
Task SaveAsync(AirgapImportRecord record, CancellationToken cancellationToken);
}
public sealed class DuplicateAirgapImportException : Exception
{
public string BundleId { get; }
public string MirrorGeneration { get; }
public DuplicateAirgapImportException(string bundleId, string mirrorGeneration, Exception inner)
: base($"Airgap import already exists for bundle '{bundleId}' generation '{mirrorGeneration}'.", inner)
{
BundleId = bundleId;
MirrorGeneration = mirrorGeneration;
}
}
internal sealed class MongoAirgapImportStore : IAirgapImportStore
{
private readonly IMongoCollection<AirgapImportRecord> _collection;
@@ -19,11 +32,30 @@ internal sealed class MongoAirgapImportStore : IAirgapImportStore
ArgumentNullException.ThrowIfNull(database);
VexMongoMappingRegistry.Register();
_collection = database.GetCollection<AirgapImportRecord>(VexMongoCollectionNames.AirgapImports);
// Enforce idempotency on (bundleId, generation) via Id uniqueness and explicit index.
var idIndex = Builders<AirgapImportRecord>.IndexKeys.Ascending(x => x.Id);
var bundleIndex = Builders<AirgapImportRecord>.IndexKeys
.Ascending(x => x.BundleId)
.Ascending(x => x.MirrorGeneration);
_collection.Indexes.CreateMany(new[]
{
new CreateIndexModel<AirgapImportRecord>(idIndex, new CreateIndexOptions { Unique = true, Name = "airgap_import_id_unique" }),
new CreateIndexModel<AirgapImportRecord>(bundleIndex, new CreateIndexOptions { Unique = true, Name = "airgap_bundle_generation_unique" })
});
}
public Task SaveAsync(AirgapImportRecord record, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(record);
return _collection.InsertOneAsync(record, cancellationToken: cancellationToken);
try
{
return _collection.InsertOneAsync(record, cancellationToken: cancellationToken);
}
catch (MongoWriteException ex) when (ex.WriteError.Category == ServerErrorCategory.DuplicateKey)
{
throw new DuplicateAirgapImportException(record.BundleId, record.MirrorGeneration, ex);
}
}
}

View File

@@ -124,11 +124,6 @@ public sealed class MongoVexRawStore : IVexRawStore
var sessionHandle = session ?? await _sessionProvider.StartSessionAsync(cancellationToken).ConfigureAwait(false);
if (!useInline)
{
newGridId = await UploadToGridFsAsync(document, sessionHandle, cancellationToken).ConfigureAwait(false);
}
var supportsTransactions = sessionHandle.Client.Cluster.Description.Type != ClusterType.Standalone
&& !sessionHandle.IsInTransaction;
@@ -183,6 +178,18 @@ public sealed class MongoVexRawStore : IVexRawStore
IngestionTelemetry.RecordLatency(tenant, sourceVendor, IngestionTelemetry.PhaseFetch, fetchWatch.Elapsed);
}
// Append-only: if the digest already exists, skip write
if (existing is not null)
{
IngestionTelemetry.RecordWriteAttempt(tenant, sourceVendor, IngestionTelemetry.ResultNoop);
return;
}
if (!useInline)
{
newGridId = await UploadToGridFsAsync(document, sessionHandle, cancellationToken).ConfigureAwait(false);
}
var record = VexRawDocumentRecord.FromDomain(document, includeContent: useInline);
record.GridFsObjectId = useInline ? null : newGridId;