feat: Add initial implementation of Vulnerability Resolver Jobs
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

- Created project for StellaOps.Scanner.Analyzers.Native.Tests with necessary dependencies.
- Documented roles and guidelines in AGENTS.md for Scheduler module.
- Implemented IResolverJobService interface and InMemoryResolverJobService for handling resolver jobs.
- Added ResolverBacklogNotifier and ResolverBacklogService for monitoring job metrics.
- Developed API endpoints for managing resolver jobs and retrieving metrics.
- Defined models for resolver job requests and responses.
- Integrated dependency injection for resolver job services.
- Implemented ImpactIndexSnapshot for persisting impact index data.
- Introduced SignalsScoringOptions for configurable scoring weights in reachability scoring.
- Added unit tests for ReachabilityScoringService and RuntimeFactsIngestionService.
- Created dotnet-filter.sh script to handle command-line arguments for dotnet.
- Established nuget-prime project for managing package downloads.
This commit is contained in:
master
2025-11-18 07:52:15 +02:00
parent e69b57d467
commit 8355e2ff75
299 changed files with 13293 additions and 2444 deletions

View File

@@ -0,0 +1,126 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using StellaOps.Excititor.Core.Observations;
using Xunit;
namespace StellaOps.Excititor.Core.Tests.Observations;
public sealed class VexLinksetUpdatedEventFactoryTests
{
[Fact]
public void Create_Normalizes_Sorts_And_Deduplicates()
{
var now = DateTimeOffset.UtcNow;
var observations = new List<VexObservation>
{
CreateObservation("obs-2", "provider-b", VexClaimStatus.Affected, 0.8, now),
CreateObservation("obs-1", "provider-a", VexClaimStatus.NotAffected, 0.1, now),
CreateObservation("obs-1", "provider-a", VexClaimStatus.NotAffected, 0.1, now), // duplicate
};
var disagreements = new[]
{
new VexObservationDisagreement("provider-b", "affected", "reason", 1.2),
new VexObservationDisagreement("provider-b", "affected", "reason", 1.2),
new VexObservationDisagreement("provider-a", "not_affected", null, -0.5),
};
var evt = VexLinksetUpdatedEventFactory.Create(
tenant: "TENANT",
linksetId: "link-123",
vulnerabilityId: "CVE-2025-0001",
productKey: "pkg:demo/app",
observations,
disagreements,
now);
Assert.Equal(VexLinksetUpdatedEventFactory.EventType, evt.EventType);
Assert.Equal("tenant", evt.Tenant);
Assert.Equal(2, evt.Observations.Length);
Assert.Collection(evt.Observations,
first =>
{
Assert.Equal("obs-1", first.ObservationId);
Assert.Equal("provider-a", first.ProviderId);
Assert.Equal("not_affected", first.Status);
Assert.Equal(0.1, first.Confidence);
},
second =>
{
Assert.Equal("obs-2", second.ObservationId);
Assert.Equal("provider-b", second.ProviderId);
Assert.Equal("affected", second.Status);
Assert.Equal(0.8, second.Confidence);
});
Assert.Equal(2, evt.Disagreements.Length);
Assert.Collection(evt.Disagreements,
first =>
{
Assert.Equal("provider-a", first.ProviderId);
Assert.Equal("not_affected", first.Status);
Assert.Equal(0.0, first.Confidence); // clamped
},
second =>
{
Assert.Equal("provider-b", second.ProviderId);
Assert.Equal("affected", second.Status);
Assert.Equal(1.0, second.Confidence); // clamped
Assert.Equal("reason", second.Justification);
});
}
private static VexObservation CreateObservation(
string observationId,
string providerId,
VexClaimStatus status,
double? severity,
DateTimeOffset createdAt)
{
var statement = new VexObservationStatement(
vulnerabilityId: "CVE-2025-0001",
productKey: "pkg:demo/app",
status: status,
lastObserved: createdAt,
purl: "pkg:demo/app",
cpe: null,
evidence: ImmutableArray<System.Text.Json.Nodes.JsonNode>.Empty,
signals: severity is null
? null
: new VexSignalSnapshot(new VexSeveritySignal("cvss", severity, "n/a", vector: null), Kev: null, Epss: null));
var upstream = new VexObservationUpstream(
upstreamId: observationId,
documentVersion: null,
fetchedAt: createdAt,
receivedAt: createdAt,
contentHash: $"sha256:{observationId}",
signature: new VexObservationSignature(true, "sub", "iss", createdAt));
var linkset = new VexObservationLinkset(
aliases: null,
purls: new[] { "pkg:demo/app" },
cpes: null,
references: null);
var content = new VexObservationContent(
format: "csaf",
specVersion: "2.0",
raw: System.Text.Json.Nodes.JsonNode.Parse("{}")!);
return new VexObservation(
observationId,
tenant: "tenant",
providerId,
streamId: "stream",
upstream,
statements: ImmutableArray.Create(statement),
content,
linkset,
createdAt,
supersedes: ImmutableArray<string>.Empty,
attributes: ImmutableDictionary<string, string>.Empty);
}
}

View File

@@ -0,0 +1,64 @@
using System.Collections.Generic;
using System.Linq;
using StellaOps.Excititor.Core.Observations;
using Xunit;
namespace StellaOps.Excititor.Core.Tests.Observations;
public sealed class VexObservationLinksetTests
{
[Fact]
public void Disagreements_Normalize_SortsAndClamps()
{
var disagreements = new[]
{
new VexObservationDisagreement("Provider-B", "affected", "just", 1.2),
new VexObservationDisagreement("provider-a", "not_affected", null, -0.1),
new VexObservationDisagreement("provider-a", "not_affected", null, 0.5),
};
var linkset = new VexObservationLinkset(
aliases: null,
purls: null,
cpes: null,
references: null,
reconciledFrom: null,
disagreements: disagreements);
Assert.Equal(2, linkset.Disagreements.Length);
var first = linkset.Disagreements[0];
Assert.Equal("provider-a", first.ProviderId);
Assert.Equal("not_affected", first.Status);
Assert.Null(first.Justification);
Assert.Equal(0.0, first.Confidence); // clamped from -0.1
var second = linkset.Disagreements[1];
Assert.Equal("Provider-B", second.ProviderId);
Assert.Equal("affected", second.Status);
Assert.Equal("just", second.Justification);
Assert.Equal(1.0, second.Confidence); // clamped from 1.2
}
[Fact]
public void Disagreements_Deduplicates_ByProviderStatusJustificationConfidence()
{
var disagreements = new List<VexObservationDisagreement>
{
new("provider-a", "affected", null, 0.7),
new("provider-a", "affected", null, 0.7),
new("provider-a", "affected", null, 0.7),
};
var linkset = new VexObservationLinkset(
aliases: null,
purls: null,
cpes: null,
references: null,
reconciledFrom: null,
disagreements: disagreements);
Assert.Single(linkset.Disagreements);
Assert.Equal(0.7, linkset.Disagreements[0].Confidence);
}
}

View File

@@ -21,11 +21,12 @@ public sealed class VexMongoMigrationRunnerTests : IAsyncLifetime
[Fact]
public async Task RunAsync_AppliesInitialIndexesOnce()
{
var migrations = new IVexMongoMigration[]
{
new VexInitialIndexMigration(),
new VexConsensusSignalsMigration(),
};
var migrations = new IVexMongoMigration[]
{
new VexInitialIndexMigration(),
new VexConsensusSignalsMigration(),
new VexObservationCollectionsMigration(),
};
var runner = new VexMongoMigrationRunner(_database, migrations, NullLogger<VexMongoMigrationRunner>.Instance);
await runner.RunAsync(CancellationToken.None);
@@ -33,8 +34,8 @@ public sealed class VexMongoMigrationRunnerTests : IAsyncLifetime
var appliedCollection = _database.GetCollection<VexMigrationRecord>(VexMongoCollectionNames.Migrations);
var applied = await appliedCollection.Find(FilterDefinition<VexMigrationRecord>.Empty).ToListAsync();
Assert.Equal(2, applied.Count);
Assert.Equal(migrations.Select(m => m.Id).OrderBy(id => id, StringComparer.Ordinal), applied.Select(record => record.Id).OrderBy(id => id, StringComparer.Ordinal));
Assert.Equal(3, applied.Count);
Assert.Equal(migrations.Select(m => m.Id).OrderBy(id => id, StringComparer.Ordinal), applied.Select(record => record.Id).OrderBy(id => id, StringComparer.Ordinal));
Assert.True(HasIndex(_database.GetCollection<VexRawDocumentRecord>(VexMongoCollectionNames.Raw), "ProviderId_1_Format_1_RetrievedAt_1"));
Assert.True(HasIndex(_database.GetCollection<VexProviderRecord>(VexMongoCollectionNames.Providers), "Kind_1"));
@@ -43,11 +44,19 @@ public sealed class VexMongoMigrationRunnerTests : IAsyncLifetime
Assert.True(HasIndex(_database.GetCollection<VexConsensusRecord>(VexMongoCollectionNames.Consensus), "PolicyRevisionId_1_CalculatedAt_-1"));
Assert.True(HasIndex(_database.GetCollection<VexExportManifestRecord>(VexMongoCollectionNames.Exports), "QuerySignature_1_Format_1"));
Assert.True(HasIndex(_database.GetCollection<VexCacheEntryRecord>(VexMongoCollectionNames.Cache), "QuerySignature_1_Format_1"));
Assert.True(HasIndex(_database.GetCollection<VexCacheEntryRecord>(VexMongoCollectionNames.Cache), "ExpiresAt_1"));
Assert.True(HasIndex(_database.GetCollection<VexStatementRecord>(VexMongoCollectionNames.Statements), "VulnerabilityId_1_Product.Key_1_InsertedAt_-1"));
Assert.True(HasIndex(_database.GetCollection<VexStatementRecord>(VexMongoCollectionNames.Statements), "ProviderId_1_InsertedAt_-1"));
Assert.True(HasIndex(_database.GetCollection<VexStatementRecord>(VexMongoCollectionNames.Statements), "Document.Digest_1"));
}
Assert.True(HasIndex(_database.GetCollection<VexCacheEntryRecord>(VexMongoCollectionNames.Cache), "ExpiresAt_1"));
Assert.True(HasIndex(_database.GetCollection<VexStatementRecord>(VexMongoCollectionNames.Statements), "VulnerabilityId_1_Product.Key_1_InsertedAt_-1"));
Assert.True(HasIndex(_database.GetCollection<VexStatementRecord>(VexMongoCollectionNames.Statements), "ProviderId_1_InsertedAt_-1"));
Assert.True(HasIndex(_database.GetCollection<VexStatementRecord>(VexMongoCollectionNames.Statements), "Document.Digest_1"));
Assert.True(HasIndex(_database.GetCollection<VexObservationRecord>(VexMongoCollectionNames.Observations), "Tenant_1_ObservationId_1"));
Assert.True(HasIndex(_database.GetCollection<VexObservationRecord>(VexMongoCollectionNames.Observations), "Tenant_1_VulnerabilityId_1"));
Assert.True(HasIndex(_database.GetCollection<VexObservationRecord>(VexMongoCollectionNames.Observations), "Tenant_1_ProductKey_1"));
Assert.True(HasIndex(_database.GetCollection<VexObservationRecord>(VexMongoCollectionNames.Observations), "Tenant_1_Document.Digest_1"));
Assert.True(HasIndex(_database.GetCollection<VexObservationRecord>(VexMongoCollectionNames.Observations), "Tenant_1_ProviderId_1_Status_1"));
Assert.True(HasIndex(_database.GetCollection<VexLinksetRecord>(VexMongoCollectionNames.Linksets), "Tenant_1_LinksetId_1"));
Assert.True(HasIndex(_database.GetCollection<VexLinksetRecord>(VexMongoCollectionNames.Linksets), "Tenant_1_VulnerabilityId_1"));
Assert.True(HasIndex(_database.GetCollection<VexLinksetRecord>(VexMongoCollectionNames.Linksets), "Tenant_1_ProductKey_1"));
}
private static bool HasIndex<TDocument>(IMongoCollection<TDocument> collection, string name)
{

View File

@@ -0,0 +1,88 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using System.Linq;
using StellaOps.Excititor.WebService.Contracts;
using StellaOps.Excititor.WebService.Telemetry;
using Xunit;
namespace StellaOps.Excititor.WebService.Tests;
public sealed class EvidenceTelemetryTests
{
[Fact]
public void RecordChunkOutcome_EmitsCounterAndHistogram()
{
var measurements = new List<(string Name, double Value, IReadOnlyList<KeyValuePair<string, object?>> Tags)>();
using var listener = CreateListener((instrument, value, tags) =>
{
measurements.Add((instrument.Name, value, tags.ToList()));
});
EvidenceTelemetry.RecordChunkOutcome("tenant-a", "success", chunkCount: 3, truncated: true);
Assert.Contains(measurements, m => m.Name == "excititor.vex.evidence.requests" && m.Value == 1);
Assert.Contains(measurements, m => m.Name == "excititor.vex.evidence.chunk_count" && m.Value == 3);
var requestTags = measurements.First(m => m.Name == "excititor.vex.evidence.requests").Tags.ToDictionary(kv => kv.Key, kv => kv.Value);
Assert.Equal("tenant-a", requestTags["tenant"]);
Assert.Equal("success", requestTags["outcome"]);
Assert.Equal(true, requestTags["truncated"]);
}
[Fact]
public void RecordChunkSignatureStatus_EmitsSignatureCounters()
{
var measurements = new List<(string Name, double Value, IReadOnlyList<KeyValuePair<string, object?>> Tags)>();
using var listener = CreateListener((instrument, value, tags) =>
{
measurements.Add((instrument.Name, value, tags.ToList()));
});
var now = DateTimeOffset.UtcNow;
var scope = new VexEvidenceChunkScope("pkg:demo/app", "demo", "1.0.0", "pkg:demo/app@1.0.0", null, new[] { "component-a" });
var document = new VexEvidenceChunkDocument("digest-1", "spdx", "https://example.test/vex.json", "r1");
var chunks = new List<VexEvidenceChunkResponse>
{
new("obs-1", "link-1", "CVE-2025-0001", "pkg:demo/app", "provider-a", "Affected", "just", "detail", 0.9, now.AddMinutes(-10), now, scope, document, new VexEvidenceChunkSignature("cosign", "sub", "issuer", "kid", now, null), new Dictionary<string, string>()),
new("obs-2", "link-2", "CVE-2025-0001", "pkg:demo/app", "provider-b", "NotAffected", null, null, null, now.AddMinutes(-8), now, scope, document, null, new Dictionary<string, string>()),
new("obs-3", "link-3", "CVE-2025-0001", "pkg:demo/app", "provider-c", "Affected", null, null, null, now.AddMinutes(-6), now, scope, document, new VexEvidenceChunkSignature("cosign", "sub", "issuer", "kid", null, null), new Dictionary<string, string>()),
};
EvidenceTelemetry.RecordChunkSignatureStatus("tenant-b", chunks);
AssertSignatureMeasurement(measurements, "unsigned", 1, "tenant-b");
AssertSignatureMeasurement(measurements, "unverified", 1, "tenant-b");
AssertSignatureMeasurement(measurements, "verified", 1, "tenant-b");
}
private static MeterListener CreateListener(Action<Instrument, double, ReadOnlySpan<KeyValuePair<string, object?>>> callback)
{
var listener = new MeterListener
{
InstrumentPublished = (instrument, l) =>
{
if (instrument.Meter.Name == EvidenceTelemetry.MeterName)
{
l.EnableMeasurementEvents(instrument);
}
}
};
listener.SetMeasurementEventCallback<long>((instrument, measurement, tags, _) => callback(instrument, measurement, tags));
listener.SetMeasurementEventCallback<int>((instrument, measurement, tags, _) => callback(instrument, measurement, tags));
listener.Start();
return listener;
}
private static void AssertSignatureMeasurement(IEnumerable<(string Name, double Value, IReadOnlyList<KeyValuePair<string, object?>> Tags)> measurements, string status, int expectedValue, string tenant)
{
var match = measurements.FirstOrDefault(m => m.Name == "excititor.vex.signature.status" && m.Tags.Any(t => t.Key == "status" && (string?)t.Value == status));
Assert.Equal(expectedValue, match.Value);
Assert.Contains(match.Tags, t => t.Key == "tenant" && (string?)t.Value == tenant);
}
}