up
This commit is contained in:
@@ -88,8 +88,10 @@ public sealed class CertCcMapperTests
|
||||
Id: Guid.NewGuid(),
|
||||
DocumentId: document.Id,
|
||||
SourceName: "cert-cc",
|
||||
Format: "certcc.vince.note.v1",
|
||||
SchemaVersion: "certcc.vince.note.v1",
|
||||
Payload: new BsonDocument(),
|
||||
CreatedAt: PublishedAt,
|
||||
ValidatedAt: PublishedAt.AddMinutes(1));
|
||||
|
||||
var advisory = CertCcMapper.Map(dto, document, dtoRecord, "cert-cc");
|
||||
|
||||
@@ -190,11 +190,11 @@ public sealed class SourceStateSeedProcessorTests : IAsyncLifetime
|
||||
Assert.NotNull(refreshedRecord);
|
||||
Assert.Equal(documentId, refreshedRecord!.Id);
|
||||
Assert.NotNull(refreshedRecord.PayloadId);
|
||||
Assert.NotEqual(previousGridId, refreshedRecord.PayloadId);
|
||||
Assert.NotEqual(previousGridId?.ToString(), refreshedRecord.PayloadId?.ToString());
|
||||
|
||||
var files = await filesCollection.Find(FilterDefinition<BsonDocument>.Empty).ToListAsync();
|
||||
Assert.Single(files);
|
||||
Assert.NotEqual(previousGridId, files[0]["_id"].AsObjectId);
|
||||
Assert.NotEqual(previousGridId?.ToString(), files[0]["_id"].AsObjectId.ToString());
|
||||
}
|
||||
|
||||
private SourceStateSeedProcessor CreateProcessor()
|
||||
|
||||
@@ -34,7 +34,7 @@ public sealed class SuseMapperTests
|
||||
},
|
||||
Etag: "adv-1",
|
||||
LastModified: DateTimeOffset.UtcNow,
|
||||
PayloadId: ObjectId.Empty);
|
||||
PayloadId: Guid.Empty);
|
||||
|
||||
var mapped = SuseMapper.Map(dto, document, DateTimeOffset.UtcNow);
|
||||
|
||||
|
||||
@@ -97,8 +97,10 @@ public sealed class OsvConflictFixtureTests
|
||||
Id: Guid.Parse("6f7d5ce7-cb47-40a5-8b41-8ad022b5fd5c"),
|
||||
DocumentId: document.Id,
|
||||
SourceName: OsvConnectorPlugin.SourceName,
|
||||
Format: "osv.v1",
|
||||
SchemaVersion: "osv.v1",
|
||||
Payload: new BsonDocument("id", dto.Id),
|
||||
CreatedAt: new DateTimeOffset(2025, 3, 6, 12, 0, 0, TimeSpan.Zero),
|
||||
ValidatedAt: new DateTimeOffset(2025, 3, 6, 12, 5, 0, TimeSpan.Zero));
|
||||
|
||||
var advisory = OsvMapper.Map(dto, document, dtoRecord, "npm");
|
||||
|
||||
@@ -65,7 +65,7 @@ public sealed class RuBduMapperTests
|
||||
null,
|
||||
null,
|
||||
dto.IdentifyDate,
|
||||
ObjectId.GenerateNewId());
|
||||
PayloadId: Guid.NewGuid());
|
||||
|
||||
var advisory = RuBduMapper.Map(dto, document, dto.IdentifyDate!.Value);
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ public sealed class RuNkckiMapperTests
|
||||
null,
|
||||
null,
|
||||
dto.DateUpdated,
|
||||
ObjectId.GenerateNewId());
|
||||
PayloadId: Guid.NewGuid());
|
||||
|
||||
Assert.Equal("КРИТИЧЕСКИЙ", dto.CvssRating);
|
||||
var normalizeSeverity = typeof(RuNkckiMapper).GetMethod("NormalizeSeverity", BindingFlags.NonPublic | BindingFlags.Static)!;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Concelier.Core.Jobs;
|
||||
using StellaOps.Concelier.Core.Events;
|
||||
using StellaOps.Concelier.Exporter.Json;
|
||||
@@ -15,15 +14,16 @@ using StellaOps.Concelier.Storage.Mongo.Exporting;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Cryptography.DependencyInjection;
|
||||
|
||||
namespace StellaOps.Concelier.Exporter.Json.Tests;
|
||||
|
||||
public sealed class JsonExporterDependencyInjectionRoutineTests
|
||||
{
|
||||
[Fact]
|
||||
public void Register_AddsJobDefinitionAndServices()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
using StellaOps.Provenance.Mongo;
|
||||
|
||||
namespace StellaOps.Concelier.Exporter.Json.Tests;
|
||||
|
||||
public sealed class JsonExporterDependencyInjectionRoutineTests
|
||||
{
|
||||
[Fact]
|
||||
public void Register_AddsJobDefinitionAndServices()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging();
|
||||
services.AddSingleton<IAdvisoryStore, StubAdvisoryStore>();
|
||||
services.AddSingleton<IExportStateStore, StubExportStateStore>();
|
||||
@@ -32,64 +32,60 @@ public sealed class JsonExporterDependencyInjectionRoutineTests
|
||||
services.AddOptions<JobSchedulerOptions>();
|
||||
services.Configure<CryptoHashOptions>(_ => { });
|
||||
services.AddStellaOpsCrypto();
|
||||
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>())
|
||||
.Build();
|
||||
|
||||
var routine = new JsonExporterDependencyInjectionRoutine();
|
||||
routine.Register(services, configuration);
|
||||
|
||||
using var provider = services.BuildServiceProvider();
|
||||
var optionsAccessor = provider.GetRequiredService<IOptions<JobSchedulerOptions>>();
|
||||
var options = optionsAccessor.Value;
|
||||
|
||||
Assert.True(options.Definitions.TryGetValue(JsonExportJob.JobKind, out var definition));
|
||||
Assert.Equal(typeof(JsonExportJob), definition.JobType);
|
||||
Assert.True(definition.Enabled);
|
||||
|
||||
var exporter = provider.GetRequiredService<JsonFeedExporter>();
|
||||
Assert.NotNull(exporter);
|
||||
}
|
||||
|
||||
private sealed class StubAdvisoryStore : IAdvisoryStore
|
||||
{
|
||||
public Task<IReadOnlyList<Advisory>> GetRecentAsync(int limit, CancellationToken cancellationToken, IClientSessionHandle? session = null)
|
||||
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>())
|
||||
.Build();
|
||||
|
||||
var routine = new JsonExporterDependencyInjectionRoutine();
|
||||
routine.Register(services, configuration);
|
||||
|
||||
using var provider = services.BuildServiceProvider();
|
||||
var optionsAccessor = provider.GetRequiredService<IOptions<JobSchedulerOptions>>();
|
||||
var options = optionsAccessor.Value;
|
||||
|
||||
Assert.True(options.Definitions.TryGetValue(JsonExportJob.JobKind, out var definition));
|
||||
Assert.Equal(typeof(JsonExportJob), definition.JobType);
|
||||
Assert.True(definition.Enabled);
|
||||
|
||||
var exporter = provider.GetRequiredService<JsonFeedExporter>();
|
||||
Assert.NotNull(exporter);
|
||||
}
|
||||
|
||||
private sealed class StubAdvisoryStore : IAdvisoryStore
|
||||
{
|
||||
public Task<IReadOnlyList<Advisory>> GetRecentAsync(int limit, CancellationToken cancellationToken)
|
||||
{
|
||||
_ = session;
|
||||
return Task.FromResult<IReadOnlyList<Advisory>>(Array.Empty<Advisory>());
|
||||
}
|
||||
|
||||
public Task<Advisory?> FindAsync(string advisoryKey, CancellationToken cancellationToken, IClientSessionHandle? session = null)
|
||||
public Task<Advisory?> FindAsync(string advisoryKey, CancellationToken cancellationToken)
|
||||
{
|
||||
_ = session;
|
||||
return Task.FromResult<Advisory?>(null);
|
||||
}
|
||||
|
||||
public Task UpsertAsync(Advisory advisory, CancellationToken cancellationToken, IClientSessionHandle? session = null)
|
||||
public Task UpsertAsync(Advisory advisory, CancellationToken cancellationToken)
|
||||
{
|
||||
_ = session;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public IAsyncEnumerable<Advisory> StreamAsync(CancellationToken cancellationToken, IClientSessionHandle? session = null)
|
||||
public IAsyncEnumerable<Advisory> StreamAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_ = session;
|
||||
return Enumerate(cancellationToken);
|
||||
|
||||
static async IAsyncEnumerable<Advisory> Enumerate([EnumeratorCancellation] CancellationToken ct)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
await Task.Yield();
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ct.ThrowIfCancellationRequested();
|
||||
await Task.Yield();
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class StubExportStateStore : IExportStateStore
|
||||
{
|
||||
private ExportStateRecord? _record;
|
||||
|
||||
|
||||
public Task<ExportStateRecord?> FindAsync(string id, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(_record);
|
||||
@@ -107,6 +103,9 @@ public sealed class JsonExporterDependencyInjectionRoutineTests
|
||||
public ValueTask AppendAsync(AdvisoryEventAppendRequest request, CancellationToken cancellationToken)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public ValueTask AttachStatementProvenanceAsync(Guid statementId, DsseProvenance provenance, TrustInfo trust, CancellationToken cancellationToken)
|
||||
=> ValueTask.CompletedTask;
|
||||
|
||||
public ValueTask<AdvisoryReplay> ReplayAsync(string vulnerabilityKey, DateTimeOffset? asOf, CancellationToken cancellationToken)
|
||||
{
|
||||
return ValueTask.FromResult(new AdvisoryReplay(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,6 @@ using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Concelier.Exporter.Json;
|
||||
using StellaOps.Concelier.Exporter.TrivyDb;
|
||||
using StellaOps.Concelier.Models;
|
||||
@@ -883,27 +882,23 @@ public sealed class TrivyDbFeedExporterTests : IDisposable
|
||||
_advisories = advisories;
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<Advisory>> GetRecentAsync(int limit, CancellationToken cancellationToken, IClientSessionHandle? session = null)
|
||||
public Task<IReadOnlyList<Advisory>> GetRecentAsync(int limit, CancellationToken cancellationToken)
|
||||
{
|
||||
_ = session;
|
||||
return Task.FromResult(_advisories);
|
||||
}
|
||||
|
||||
public Task<Advisory?> FindAsync(string advisoryKey, CancellationToken cancellationToken, IClientSessionHandle? session = null)
|
||||
public Task<Advisory?> FindAsync(string advisoryKey, CancellationToken cancellationToken)
|
||||
{
|
||||
_ = session;
|
||||
return Task.FromResult<Advisory?>(_advisories.FirstOrDefault(a => a.AdvisoryKey == advisoryKey));
|
||||
}
|
||||
|
||||
public Task UpsertAsync(Advisory advisory, CancellationToken cancellationToken, IClientSessionHandle? session = null)
|
||||
public Task UpsertAsync(Advisory advisory, CancellationToken cancellationToken)
|
||||
{
|
||||
_ = session;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public IAsyncEnumerable<Advisory> StreamAsync(CancellationToken cancellationToken, IClientSessionHandle? session = null)
|
||||
public IAsyncEnumerable<Advisory> StreamAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_ = session;
|
||||
return EnumerateAsync(cancellationToken);
|
||||
|
||||
async IAsyncEnumerable<Advisory> EnumerateAsync([EnumeratorCancellation] CancellationToken ct)
|
||||
|
||||
@@ -2,109 +2,109 @@ using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Concelier.Core;
|
||||
using StellaOps.Concelier.Core.Events;
|
||||
using StellaOps.Concelier.Merge.Services;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo.Aliases;
|
||||
using StellaOps.Concelier.Storage.Mongo.MergeEvents;
|
||||
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
public sealed class AdvisoryMergeServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task MergeAsync_AppliesCanonicalRulesAndPersistsDecisions()
|
||||
{
|
||||
var aliasStore = new FakeAliasStore();
|
||||
aliasStore.Register("GHSA-aaaa-bbbb-cccc",
|
||||
(AliasSchemes.Ghsa, "GHSA-aaaa-bbbb-cccc"),
|
||||
(AliasSchemes.Cve, "CVE-2025-4242"));
|
||||
aliasStore.Register("CVE-2025-4242",
|
||||
(AliasSchemes.Cve, "CVE-2025-4242"));
|
||||
aliasStore.Register("OSV-2025-xyz",
|
||||
(AliasSchemes.OsV, "OSV-2025-xyz"),
|
||||
(AliasSchemes.Cve, "CVE-2025-4242"));
|
||||
|
||||
var advisoryStore = new FakeAdvisoryStore();
|
||||
advisoryStore.Seed(CreateGhsaAdvisory(), CreateNvdAdvisory(), CreateOsvAdvisory());
|
||||
|
||||
var mergeEventStore = new InMemoryMergeEventStore();
|
||||
var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 4, 1, 0, 0, 0, TimeSpan.Zero));
|
||||
var writer = new MergeEventWriter(mergeEventStore, new CanonicalHashCalculator(), timeProvider, NullLogger<MergeEventWriter>.Instance);
|
||||
var precedenceMerger = new AdvisoryPrecedenceMerger(new AffectedPackagePrecedenceResolver(), timeProvider);
|
||||
var aliasResolver = new AliasGraphResolver(aliasStore);
|
||||
var canonicalMerger = new CanonicalMerger(timeProvider);
|
||||
var eventLog = new RecordingAdvisoryEventLog();
|
||||
var service = new AdvisoryMergeService(aliasResolver, advisoryStore, precedenceMerger, writer, canonicalMerger, eventLog, timeProvider, NullLogger<AdvisoryMergeService>.Instance);
|
||||
|
||||
var result = await service.MergeAsync("GHSA-aaaa-bbbb-cccc", CancellationToken.None);
|
||||
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Concelier.Core;
|
||||
using StellaOps.Concelier.Core.Events;
|
||||
using StellaOps.Concelier.Merge.Services;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Storage.Mongo.Aliases;
|
||||
using StellaOps.Concelier.Storage.Mongo.MergeEvents;
|
||||
using StellaOps.Provenance.Mongo;
|
||||
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
public sealed class AdvisoryMergeServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task MergeAsync_AppliesCanonicalRulesAndPersistsDecisions()
|
||||
{
|
||||
var aliasStore = new FakeAliasStore();
|
||||
aliasStore.Register("GHSA-aaaa-bbbb-cccc",
|
||||
(AliasSchemes.Ghsa, "GHSA-aaaa-bbbb-cccc"),
|
||||
(AliasSchemes.Cve, "CVE-2025-4242"));
|
||||
aliasStore.Register("CVE-2025-4242",
|
||||
(AliasSchemes.Cve, "CVE-2025-4242"));
|
||||
aliasStore.Register("OSV-2025-xyz",
|
||||
(AliasSchemes.OsV, "OSV-2025-xyz"),
|
||||
(AliasSchemes.Cve, "CVE-2025-4242"));
|
||||
|
||||
var advisoryStore = new FakeAdvisoryStore();
|
||||
advisoryStore.Seed(CreateGhsaAdvisory(), CreateNvdAdvisory(), CreateOsvAdvisory());
|
||||
|
||||
var mergeEventStore = new InMemoryMergeEventStore();
|
||||
var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 4, 1, 0, 0, 0, TimeSpan.Zero));
|
||||
var writer = new MergeEventWriter(mergeEventStore, new CanonicalHashCalculator(), timeProvider, NullLogger<MergeEventWriter>.Instance);
|
||||
var precedenceMerger = new AdvisoryPrecedenceMerger(new AffectedPackagePrecedenceResolver(), timeProvider);
|
||||
var aliasResolver = new AliasGraphResolver(aliasStore);
|
||||
var canonicalMerger = new CanonicalMerger(timeProvider);
|
||||
var eventLog = new RecordingAdvisoryEventLog();
|
||||
var service = new AdvisoryMergeService(aliasResolver, advisoryStore, precedenceMerger, writer, canonicalMerger, eventLog, timeProvider, NullLogger<AdvisoryMergeService>.Instance);
|
||||
|
||||
var result = await service.MergeAsync("GHSA-aaaa-bbbb-cccc", CancellationToken.None);
|
||||
|
||||
Assert.NotNull(result.Merged);
|
||||
Assert.Equal("OSV summary overrides", result.Merged!.Summary);
|
||||
Assert.Empty(result.Conflicts);
|
||||
|
||||
var upserted = advisoryStore.LastUpserted;
|
||||
Assert.NotNull(upserted);
|
||||
Assert.Equal("CVE-2025-4242", upserted!.AdvisoryKey);
|
||||
Assert.Equal("OSV summary overrides", upserted.Summary);
|
||||
|
||||
var mergeRecord = mergeEventStore.LastRecord;
|
||||
Assert.NotNull(mergeRecord);
|
||||
var summaryDecision = Assert.Single(mergeRecord!.FieldDecisions, decision => decision.Field == "summary");
|
||||
Assert.Equal("osv", summaryDecision.SelectedSource);
|
||||
Assert.Equal("freshness_override", summaryDecision.DecisionReason);
|
||||
|
||||
var appendRequest = eventLog.LastRequest;
|
||||
Assert.NotNull(appendRequest);
|
||||
Assert.Contains(appendRequest!.Statements, statement => string.Equals(statement.Advisory.AdvisoryKey, "CVE-2025-4242", StringComparison.OrdinalIgnoreCase));
|
||||
Assert.True(appendRequest.Conflicts is null || appendRequest.Conflicts.Count == 0);
|
||||
}
|
||||
|
||||
private static Advisory CreateGhsaAdvisory()
|
||||
{
|
||||
var recorded = DateTimeOffset.Parse("2025-03-01T00:00:00Z");
|
||||
var provenance = new AdvisoryProvenance("ghsa", "map", "GHSA-aaaa-bbbb-cccc", recorded, new[] { ProvenanceFieldMasks.Advisory });
|
||||
return new Advisory(
|
||||
"GHSA-aaaa-bbbb-cccc",
|
||||
"Container escape",
|
||||
"Initial GHSA summary.",
|
||||
"en",
|
||||
recorded,
|
||||
recorded,
|
||||
"medium",
|
||||
exploitKnown: false,
|
||||
aliases: new[] { "CVE-2025-4242", "GHSA-aaaa-bbbb-cccc" },
|
||||
references: Array.Empty<AdvisoryReference>(),
|
||||
affectedPackages: Array.Empty<AffectedPackage>(),
|
||||
cvssMetrics: Array.Empty<CvssMetric>(),
|
||||
provenance: new[] { provenance });
|
||||
}
|
||||
|
||||
private static Advisory CreateNvdAdvisory()
|
||||
{
|
||||
var recorded = DateTimeOffset.Parse("2025-03-02T00:00:00Z");
|
||||
var provenance = new AdvisoryProvenance("nvd", "map", "CVE-2025-4242", recorded, new[] { ProvenanceFieldMasks.Advisory });
|
||||
return new Advisory(
|
||||
"CVE-2025-4242",
|
||||
"CVE-2025-4242",
|
||||
"Baseline NVD summary.",
|
||||
"en",
|
||||
recorded,
|
||||
recorded,
|
||||
"high",
|
||||
exploitKnown: false,
|
||||
aliases: new[] { "CVE-2025-4242" },
|
||||
references: Array.Empty<AdvisoryReference>(),
|
||||
affectedPackages: Array.Empty<AffectedPackage>(),
|
||||
cvssMetrics: Array.Empty<CvssMetric>(),
|
||||
provenance: new[] { provenance });
|
||||
}
|
||||
|
||||
|
||||
var upserted = advisoryStore.LastUpserted;
|
||||
Assert.NotNull(upserted);
|
||||
Assert.Equal("CVE-2025-4242", upserted!.AdvisoryKey);
|
||||
Assert.Equal("OSV summary overrides", upserted.Summary);
|
||||
|
||||
var mergeRecord = mergeEventStore.LastRecord;
|
||||
Assert.NotNull(mergeRecord);
|
||||
var summaryDecision = Assert.Single(mergeRecord!.FieldDecisions, decision => decision.Field == "summary");
|
||||
Assert.Equal("osv", summaryDecision.SelectedSource);
|
||||
Assert.Equal("freshness_override", summaryDecision.DecisionReason);
|
||||
|
||||
var appendRequest = eventLog.LastRequest;
|
||||
Assert.NotNull(appendRequest);
|
||||
Assert.Contains(appendRequest!.Statements, statement => string.Equals(statement.Advisory.AdvisoryKey, "CVE-2025-4242", StringComparison.OrdinalIgnoreCase));
|
||||
Assert.True(appendRequest.Conflicts is null || appendRequest.Conflicts.Count == 0);
|
||||
}
|
||||
|
||||
private static Advisory CreateGhsaAdvisory()
|
||||
{
|
||||
var recorded = DateTimeOffset.Parse("2025-03-01T00:00:00Z");
|
||||
var provenance = new AdvisoryProvenance("ghsa", "map", "GHSA-aaaa-bbbb-cccc", recorded, new[] { ProvenanceFieldMasks.Advisory });
|
||||
return new Advisory(
|
||||
"GHSA-aaaa-bbbb-cccc",
|
||||
"Container escape",
|
||||
"Initial GHSA summary.",
|
||||
"en",
|
||||
recorded,
|
||||
recorded,
|
||||
"medium",
|
||||
exploitKnown: false,
|
||||
aliases: new[] { "CVE-2025-4242", "GHSA-aaaa-bbbb-cccc" },
|
||||
references: Array.Empty<AdvisoryReference>(),
|
||||
affectedPackages: Array.Empty<AffectedPackage>(),
|
||||
cvssMetrics: Array.Empty<CvssMetric>(),
|
||||
provenance: new[] { provenance });
|
||||
}
|
||||
|
||||
private static Advisory CreateNvdAdvisory()
|
||||
{
|
||||
var recorded = DateTimeOffset.Parse("2025-03-02T00:00:00Z");
|
||||
var provenance = new AdvisoryProvenance("nvd", "map", "CVE-2025-4242", recorded, new[] { ProvenanceFieldMasks.Advisory });
|
||||
return new Advisory(
|
||||
"CVE-2025-4242",
|
||||
"CVE-2025-4242",
|
||||
"Baseline NVD summary.",
|
||||
"en",
|
||||
recorded,
|
||||
recorded,
|
||||
"high",
|
||||
exploitKnown: false,
|
||||
aliases: new[] { "CVE-2025-4242" },
|
||||
references: Array.Empty<AdvisoryReference>(),
|
||||
affectedPackages: Array.Empty<AffectedPackage>(),
|
||||
cvssMetrics: Array.Empty<CvssMetric>(),
|
||||
provenance: new[] { provenance });
|
||||
}
|
||||
|
||||
private static Advisory CreateOsvAdvisory()
|
||||
{
|
||||
var recorded = DateTimeOffset.Parse("2025-03-05T12:00:00Z");
|
||||
@@ -207,120 +207,119 @@ public sealed class AdvisoryMergeServiceTests
|
||||
Assert.Equal(conflict.ConflictId, appendedConflict.ConflictId);
|
||||
Assert.Equal(conflict.StatementIds, appendedConflict.StatementIds.ToImmutableArray());
|
||||
}
|
||||
|
||||
|
||||
private sealed class RecordingAdvisoryEventLog : IAdvisoryEventLog
|
||||
{
|
||||
public AdvisoryEventAppendRequest? LastRequest { get; private set; }
|
||||
|
||||
public ValueTask AppendAsync(AdvisoryEventAppendRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
LastRequest = request;
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public ValueTask<AdvisoryReplay> ReplayAsync(string vulnerabilityKey, DateTimeOffset? asOf, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FakeAliasStore : IAliasStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, List<AliasRecord>> _records = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public void Register(string advisoryKey, params (string Scheme, string Value)[] aliases)
|
||||
{
|
||||
var list = new List<AliasRecord>();
|
||||
foreach (var (scheme, value) in aliases)
|
||||
{
|
||||
list.Add(new AliasRecord(advisoryKey, scheme, value, DateTimeOffset.UtcNow));
|
||||
}
|
||||
|
||||
_records[advisoryKey] = list;
|
||||
}
|
||||
|
||||
public Task<AliasUpsertResult> ReplaceAsync(string advisoryKey, IEnumerable<AliasEntry> aliases, DateTimeOffset updatedAt, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(new AliasUpsertResult(advisoryKey, Array.Empty<AliasCollision>()));
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<AliasRecord>> GetByAliasAsync(string scheme, string value, CancellationToken cancellationToken)
|
||||
{
|
||||
var matches = _records.Values
|
||||
.SelectMany(static records => records)
|
||||
.Where(record => string.Equals(record.Scheme, scheme, StringComparison.OrdinalIgnoreCase) && string.Equals(record.Value, value, StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult<IReadOnlyList<AliasRecord>>(matches);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<AliasRecord>> GetByAdvisoryAsync(string advisoryKey, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_records.TryGetValue(advisoryKey, out var records))
|
||||
{
|
||||
return Task.FromResult<IReadOnlyList<AliasRecord>>(records);
|
||||
}
|
||||
|
||||
return Task.FromResult<IReadOnlyList<AliasRecord>>(Array.Empty<AliasRecord>());
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FakeAdvisoryStore : IAdvisoryStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, Advisory> _advisories = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public Advisory? LastUpserted { get; private set; }
|
||||
|
||||
public void Seed(params Advisory[] advisories)
|
||||
{
|
||||
foreach (var advisory in advisories)
|
||||
{
|
||||
_advisories[advisory.AdvisoryKey] = advisory;
|
||||
}
|
||||
}
|
||||
|
||||
public Task<Advisory?> FindAsync(string advisoryKey, CancellationToken cancellationToken, IClientSessionHandle? session = null)
|
||||
{
|
||||
_ = session;
|
||||
_advisories.TryGetValue(advisoryKey, out var advisory);
|
||||
return Task.FromResult(advisory);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<Advisory>> GetRecentAsync(int limit, CancellationToken cancellationToken, IClientSessionHandle? session = null)
|
||||
{
|
||||
_ = session;
|
||||
return Task.FromResult<IReadOnlyList<Advisory>>(Array.Empty<Advisory>());
|
||||
}
|
||||
|
||||
public Task UpsertAsync(Advisory advisory, CancellationToken cancellationToken, IClientSessionHandle? session = null)
|
||||
{
|
||||
_ = session;
|
||||
_advisories[advisory.AdvisoryKey] = advisory;
|
||||
LastUpserted = advisory;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public IAsyncEnumerable<Advisory> StreamAsync(CancellationToken cancellationToken, IClientSessionHandle? session = null)
|
||||
{
|
||||
_ = session;
|
||||
return AsyncEnumerable.Empty<Advisory>();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class InMemoryMergeEventStore : IMergeEventStore
|
||||
{
|
||||
public MergeEventRecord? LastRecord { get; private set; }
|
||||
|
||||
public Task AppendAsync(MergeEventRecord record, CancellationToken cancellationToken)
|
||||
{
|
||||
LastRecord = record;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<MergeEventRecord>> GetRecentAsync(string advisoryKey, int limit, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult<IReadOnlyList<MergeEventRecord>>(Array.Empty<MergeEventRecord>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private sealed class RecordingAdvisoryEventLog : IAdvisoryEventLog
|
||||
{
|
||||
public AdvisoryEventAppendRequest? LastRequest { get; private set; }
|
||||
|
||||
public ValueTask AppendAsync(AdvisoryEventAppendRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
LastRequest = request;
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public ValueTask AttachStatementProvenanceAsync(Guid statementId, DsseProvenance provenance, TrustInfo trust, CancellationToken cancellationToken)
|
||||
=> ValueTask.CompletedTask;
|
||||
|
||||
public ValueTask<AdvisoryReplay> ReplayAsync(string vulnerabilityKey, DateTimeOffset? asOf, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FakeAliasStore : IAliasStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, List<AliasRecord>> _records = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public void Register(string advisoryKey, params (string Scheme, string Value)[] aliases)
|
||||
{
|
||||
var list = new List<AliasRecord>();
|
||||
foreach (var (scheme, value) in aliases)
|
||||
{
|
||||
list.Add(new AliasRecord(advisoryKey, scheme, value, DateTimeOffset.UtcNow));
|
||||
}
|
||||
|
||||
_records[advisoryKey] = list;
|
||||
}
|
||||
|
||||
public Task<AliasUpsertResult> ReplaceAsync(string advisoryKey, IEnumerable<AliasEntry> aliases, DateTimeOffset updatedAt, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(new AliasUpsertResult(advisoryKey, Array.Empty<AliasCollision>()));
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<AliasRecord>> GetByAliasAsync(string scheme, string value, CancellationToken cancellationToken)
|
||||
{
|
||||
var matches = _records.Values
|
||||
.SelectMany(static records => records)
|
||||
.Where(record => string.Equals(record.Scheme, scheme, StringComparison.OrdinalIgnoreCase) && string.Equals(record.Value, value, StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult<IReadOnlyList<AliasRecord>>(matches);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<AliasRecord>> GetByAdvisoryAsync(string advisoryKey, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_records.TryGetValue(advisoryKey, out var records))
|
||||
{
|
||||
return Task.FromResult<IReadOnlyList<AliasRecord>>(records);
|
||||
}
|
||||
|
||||
return Task.FromResult<IReadOnlyList<AliasRecord>>(Array.Empty<AliasRecord>());
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FakeAdvisoryStore : IAdvisoryStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, Advisory> _advisories = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public Advisory? LastUpserted { get; private set; }
|
||||
|
||||
public void Seed(params Advisory[] advisories)
|
||||
{
|
||||
foreach (var advisory in advisories)
|
||||
{
|
||||
_advisories[advisory.AdvisoryKey] = advisory;
|
||||
}
|
||||
}
|
||||
|
||||
public Task<Advisory?> FindAsync(string advisoryKey, CancellationToken cancellationToken)
|
||||
{
|
||||
_advisories.TryGetValue(advisoryKey, out var advisory);
|
||||
return Task.FromResult(advisory);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<Advisory>> GetRecentAsync(int limit, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult<IReadOnlyList<Advisory>>(Array.Empty<Advisory>());
|
||||
}
|
||||
|
||||
public Task UpsertAsync(Advisory advisory, CancellationToken cancellationToken)
|
||||
{
|
||||
_advisories[advisory.AdvisoryKey] = advisory;
|
||||
LastUpserted = advisory;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public IAsyncEnumerable<Advisory> StreamAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return AsyncEnumerable.Empty<Advisory>();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class InMemoryMergeEventStore : IMergeEventStore
|
||||
{
|
||||
public MergeEventRecord? LastRecord { get; private set; }
|
||||
|
||||
public Task AppendAsync(MergeEventRecord record, CancellationToken cancellationToken)
|
||||
{
|
||||
LastRecord = record;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<MergeEventRecord>> GetRecentAsync(string advisoryKey, int limit, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult<IReadOnlyList<MergeEventRecord>>(Array.Empty<MergeEventRecord>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit.v3" Version="3.0.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.3" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
@@ -24,4 +24,4 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.RawModels/StellaOps.Concelier.RawModels.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Options;
|
||||
using OptionsFactory = Microsoft.Extensions.Options.Options;
|
||||
using StellaOps.Aoc;
|
||||
using StellaOps.Concelier.Core.Aoc;
|
||||
|
||||
@@ -194,7 +195,7 @@ public sealed class AocVerifyRegressionTests
|
||||
public void Verify_MapperGuardParity_ValidationResultsMatch()
|
||||
{
|
||||
var guard = new AocWriteGuard();
|
||||
var validator = new AdvisorySchemaValidator(guard, Options.Create(GuardOptions));
|
||||
var validator = new AdvisorySchemaValidator(guard, OptionsFactory.Create(GuardOptions));
|
||||
|
||||
// Create document with forbidden field
|
||||
var json = CreateJsonWithForbiddenField("severity", "high");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Options;
|
||||
using OptionsFactory = Microsoft.Extensions.Options.Options;
|
||||
using StellaOps.Aoc;
|
||||
using StellaOps.Concelier.Core.Aoc;
|
||||
using StellaOps.Concelier.RawModels;
|
||||
@@ -43,7 +44,7 @@ public sealed class LargeBatchIngestTests
|
||||
for (int i = 0; i < results1.Count; i++)
|
||||
{
|
||||
Assert.Equal(results1[i].IsValid, results2[i].IsValid);
|
||||
Assert.Equal(results1[i].Violations.Count, results2[i].Violations.Count);
|
||||
Assert.Equal(results1[i].Violations.Length, results2[i].Violations.Length);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,8 +64,8 @@ public sealed class LargeBatchIngestTests
|
||||
var violations1 = results1[i].Violations;
|
||||
var violations2 = results2[i].Violations;
|
||||
|
||||
Assert.Equal(violations1.Count, violations2.Count);
|
||||
for (int j = 0; j < violations1.Count; j++)
|
||||
Assert.Equal(violations1.Length, violations2.Length);
|
||||
for (int j = 0; j < violations1.Length; j++)
|
||||
{
|
||||
Assert.Equal(violations1[j].ErrorCode, violations2[j].ErrorCode);
|
||||
Assert.Equal(violations1[j].Path, violations2[j].Path);
|
||||
@@ -150,15 +151,15 @@ public sealed class LargeBatchIngestTests
|
||||
// Same generation should produce same violation counts
|
||||
var validCount1 = results1.Count(r => r.IsValid);
|
||||
var validCount2 = results2.Count(r => r.IsValid);
|
||||
var violationCount1 = results1.Sum(r => r.Violations.Count);
|
||||
var violationCount2 = results2.Sum(r => r.Violations.Count);
|
||||
var violationCount1 = results1.Sum(r => r.Violations.Length);
|
||||
var violationCount2 = results2.Sum(r => r.Violations.Length);
|
||||
|
||||
Assert.Equal(validCount1, validCount2);
|
||||
Assert.Equal(violationCount1, violationCount2);
|
||||
}
|
||||
|
||||
private static AdvisorySchemaValidator CreateValidator()
|
||||
=> new(new AocWriteGuard(), Options.Create(GuardOptions));
|
||||
=> new(new AocWriteGuard(), OptionsFactory.Create(GuardOptions));
|
||||
|
||||
private static List<AdvisoryRawDocument> GenerateValidDocuments(int count)
|
||||
{
|
||||
|
||||
@@ -171,5 +171,27 @@ public sealed class AdvisoryChunkBuilderTests
|
||||
var bytes = await ComputeHashAsync(stream, algorithmId, cancellationToken).ConfigureAwait(false);
|
||||
return Convert.ToHexString(bytes).ToLowerInvariant();
|
||||
}
|
||||
|
||||
public byte[] ComputeHashForPurpose(ReadOnlySpan<byte> data, string purpose) => ComputeHash(data, purpose);
|
||||
|
||||
public string ComputeHashHexForPurpose(ReadOnlySpan<byte> data, string purpose) => ComputeHashHex(data, purpose);
|
||||
|
||||
public string ComputeHashBase64ForPurpose(ReadOnlySpan<byte> data, string purpose) => ComputeHashBase64(data, purpose);
|
||||
|
||||
public ValueTask<byte[]> ComputeHashForPurposeAsync(Stream stream, string purpose, CancellationToken cancellationToken = default)
|
||||
=> ComputeHashAsync(stream, purpose, cancellationToken);
|
||||
|
||||
public ValueTask<string> ComputeHashHexForPurposeAsync(Stream stream, string purpose, CancellationToken cancellationToken = default)
|
||||
=> ComputeHashHexAsync(stream, purpose, cancellationToken);
|
||||
|
||||
public string GetAlgorithmForPurpose(string purpose) => purpose ?? "sha256";
|
||||
|
||||
public string GetHashPrefix(string purpose) => $"{(purpose ?? "sha256").ToLowerInvariant()}:";
|
||||
|
||||
public string ComputePrefixedHashForPurpose(ReadOnlySpan<byte> data, string purpose)
|
||||
{
|
||||
var hash = ComputeHashHexForPurpose(data, purpose);
|
||||
return $"{GetHashPrefix(purpose)}{hash}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user