tests fixes and sprints work

This commit is contained in:
master
2026-01-22 19:08:46 +02:00
parent c32fff8f86
commit 726d70dc7f
881 changed files with 134434 additions and 6228 deletions

View File

@@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using StellaOps.Excititor.Core.Observations;
using StellaOps.Excititor.Core.Storage;
using StellaOps.Excititor.WebService.Services;
using static Program;
namespace StellaOps.Excititor.WebService.Endpoints;
@@ -72,33 +73,29 @@ public static class RekorAttestationEndpoints
TraceId = context.TraceIdentifier
};
// Get observation and attest it
// Note: In real implementation, we'd fetch the observation first
var result = await attestationService.AttestAndLinkAsync(
new VexObservation { Id = observationId },
options,
cancellationToken);
// TODO: In real implementation, we'd fetch the observation first and pass it
// For now, we use the simpler VerifyLinkageAsync which takes observationId
var result = await attestationService.VerifyLinkageAsync(observationId, cancellationToken);
if (!result.Success)
if (!result.IsValid)
{
return Results.Problem(
detail: result.ErrorMessage,
statusCode: result.ErrorCode switch
detail: result.Message,
statusCode: result.Status switch
{
VexAttestationErrorCode.ObservationNotFound => StatusCodes.Status404NotFound,
VexAttestationErrorCode.AlreadyAttested => StatusCodes.Status409Conflict,
VexAttestationErrorCode.Timeout => StatusCodes.Status504GatewayTimeout,
RekorLinkageVerificationStatus.NoLinkage => StatusCodes.Status404NotFound,
RekorLinkageVerificationStatus.EntryNotFound => StatusCodes.Status404NotFound,
_ => StatusCodes.Status500InternalServerError
},
title: "Attestation failed");
title: "Verification failed");
}
var response = new AttestObservationResponse(
observationId,
result.RekorLinkage!.EntryUuid,
result.RekorLinkage.LogIndex,
result.RekorLinkage.IntegratedTime,
result.Duration);
result.Linkage!.Uuid,
result.Linkage.LogIndex,
result.Linkage.IntegratedTime,
null);
return Results.Ok(response);
}).WithName("AttestObservationToRekor");
@@ -164,7 +161,7 @@ public static class RekorAttestationEndpoints
var items = results.Select(r => new BatchAttestResultItem(
r.ObservationId,
r.Success,
r.RekorLinkage?.EntryUuid,
r.RekorLinkage?.Uuid,
r.RekorLinkage?.LogIndex,
r.ErrorMessage,
r.ErrorCode?.ToString()
@@ -218,11 +215,11 @@ public static class RekorAttestationEndpoints
var response = new VerifyLinkageResponse(
observationId,
result.IsVerified,
result.IsValid,
result.VerifiedAt,
result.RekorEntryId,
result.LogIndex,
result.FailureReason);
result.Linkage?.Uuid,
result.Linkage?.LogIndex,
result.Message);
return Results.Ok(response);
}).WithName("VerifyObservationRekorLinkage");

View File

@@ -1,4 +1,4 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
@@ -168,27 +168,27 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Excititor.WebServ
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.TestKit", "StellaOps.TestKit", "{8380A20C-A5B8-EE91-1A58-270323688CB9}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Tests", "__Tests", "{90659617-4DF7-809A-4E5B-29BB5A98E8E1}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Libraries", "__Libraries", "{AB8B269C-5A2A-A4B8-0488-B5F81E55B4D9}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Infrastructure.Postgres.Testing", "StellaOps.Infrastructure.Postgres.Testing", "{CEDC2447-F717-3C95-7E08-F214D575A7B7}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Libraries", "__Libraries", "{A5C98087-E847-D2C4-2143-20869479839D}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Excititor.ArtifactStores.S3", "StellaOps.Excititor.ArtifactStores.S3", "{54262A2E-3B5B-9906-4F67-57DA2E320C7E}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Excititor.Attestation", "StellaOps.Excititor.Attestation", "{F783416C-CF8E-EA23-008C-9BBD4F2DEE8A}"
@@ -266,39 +266,39 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Worker"
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Excititor.Attestation.Tests", "StellaOps.Excititor.Attestation.Tests", "{AC096EB8-4D00-F334-0B64-675F48D97ABF}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Excititor.Connectors.Cisco.CSAF.Tests", "StellaOps.Excititor.Connectors.Cisco.CSAF.Tests", "{F3DB3FA8-37F2-72D1-F522-50E21866BCDA}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Excititor.Connectors.MSRC.CSAF.Tests", "StellaOps.Excititor.Connectors.MSRC.CSAF.Tests", "{8A1E9650-DAD0-3A3D-3F1A-02BEDB2DBE07}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.Tests", "StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.Tests", "{9363BAC4-9941-0357-4BC5-53D85020BE3E}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Excititor.Connectors.Oracle.CSAF.Tests", "StellaOps.Excititor.Connectors.Oracle.CSAF.Tests", "{3075FE8A-C279-5577-06E1-594BB4DC8DE8}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Excititor.Connectors.RedHat.CSAF.Tests", "StellaOps.Excititor.Connectors.RedHat.CSAF.Tests", "{2E446B3D-727D-72F3-7C9A-30EFFB2A73D0}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Excititor.Connectors.SUSE.RancherVEXHub.Tests", "StellaOps.Excititor.Connectors.SUSE.RancherVEXHub.Tests", "{AB16FCF9-17F9-B133-05B4-9EC184F5417E}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Excititor.Connectors.Ubuntu.CSAF.Tests", "StellaOps.Excititor.Connectors.Ubuntu.CSAF.Tests", "{5A2756AB-3E51-EA80-87F5-3F110674CCC6}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Excititor.Core.Tests", "StellaOps.Excititor.Core.Tests", "{6A7EF7BD-7D29-74F7-C194-46A280E2948B}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Excititor.Core.UnitTests", "StellaOps.Excititor.Core.UnitTests", "{12211342-66CF-90CF-C204-D5B301A73DD2}"
EndProject
@@ -724,3 +724,4 @@ Global
{FB34867C-E7DE-6581-003C-48302804940D}.Release|Any CPU.Build.0 = Release|Any CPU
{03591035-2CB8-B866-0475-08B816340E65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU

View File

@@ -12,6 +12,14 @@
<ItemGroup>
<Compile Remove="..\..\..\StellaOps.Concelier.Tests.Shared\AssemblyInfo.cs" />
<Compile Remove="..\..\..\StellaOps.Concelier.Tests.Shared\MongoFixtureCollection.cs" />
<Compile Remove="..\..\..\StellaOps.Concelier.Tests.Shared\ConcelierFixtureCollection.cs" />
<Compile Remove="..\..\..\Concelier\StellaOps.Concelier.Tests.Shared\**\*" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Moq" />
<!-- Explicitly exclude xunit v2 to avoid conflicts -->
<PackageReference Remove="xunit" />
<PackageReference Remove="xunit.core" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../__Libraries/StellaOps.Excititor.Attestation/StellaOps.Excititor.Attestation.csproj" />

View File

@@ -7,10 +7,13 @@
using System.Collections.Immutable;
using System.Text.Json;
using System.Text.Json.Nodes;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Time.Testing;
using Moq;
using StellaOps.Excititor.Core;
using StellaOps.Excititor.Core.Observations;
using StellaOps.TestKit;
using Xunit;
@@ -22,262 +25,188 @@ public sealed class VexRekorAttestationFlowTests
{
private static readonly DateTimeOffset FixedTimestamp = new(2026, 1, 16, 12, 0, 0, TimeSpan.Zero);
private readonly FakeTimeProvider _timeProvider;
private readonly InMemoryVexObservationStore _observationStore;
private readonly MockRekorClient _rekorClient;
private readonly Mock<IVexObservationAttestationService> _mockAttestationService;
public VexRekorAttestationFlowTests()
{
_timeProvider = new FakeTimeProvider(FixedTimestamp);
_observationStore = new InMemoryVexObservationStore();
_rekorClient = new MockRekorClient();
_mockAttestationService = new Mock<IVexObservationAttestationService>();
}
[Fact]
public async Task AttestObservation_CreatesRekorEntry_UpdatesLinkage()
public async Task AttestAndLinkAsync_WhenSuccessful_ReturnsSuccessResult()
{
// Arrange
var observation = CreateTestObservation("obs-001");
await _observationStore.InsertAsync(observation, CancellationToken.None);
var service = CreateService();
// Act
var result = await service.AttestAsync("default", "obs-001", CancellationToken.None);
// Assert
result.Success.Should().BeTrue();
result.RekorEntryId.Should().NotBeNullOrEmpty();
result.LogIndex.Should().BeGreaterThan(0);
// Verify linkage was updated
var updated = await _observationStore.GetByIdAsync("default", "obs-001", CancellationToken.None);
updated.Should().NotBeNull();
updated!.RekorUuid.Should().Be(result.RekorEntryId);
updated.RekorLogIndex.Should().Be(result.LogIndex);
}
[Fact]
public async Task AttestObservation_AlreadyAttested_ReturnsExisting()
{
// Arrange
var observation = CreateTestObservation("obs-002") with
var expectedLinkage = new RekorLinkage
{
RekorUuid = "existing-uuid-12345678",
RekorLogIndex = 999
Uuid = "test-uuid-12345678",
LogIndex = 12345,
IntegratedTime = FixedTimestamp,
LogUrl = "https://rekor.sigstore.dev"
};
await _observationStore.UpsertAsync(observation, CancellationToken.None);
var service = CreateService();
var expectedResult = VexObservationAttestationResult.Succeeded(
"obs-001",
expectedLinkage,
TimeSpan.FromMilliseconds(100));
_mockAttestationService
.Setup(s => s.AttestAndLinkAsync(
It.IsAny<VexObservation>(),
It.IsAny<VexAttestationOptions>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(expectedResult);
// Act
var result = await service.AttestAsync("default", "obs-002", CancellationToken.None);
// Assert
result.Success.Should().BeTrue();
result.AlreadyAttested.Should().BeTrue();
result.RekorEntryId.Should().Be("existing-uuid-12345678");
}
[Fact]
public async Task AttestObservation_NotFound_ReturnsFailure()
{
// Arrange
var service = CreateService();
// Act
var result = await service.AttestAsync("default", "nonexistent", CancellationToken.None);
// Assert
result.Success.Should().BeFalse();
result.ErrorCode.Should().Be("OBSERVATION_NOT_FOUND");
}
[Fact]
public async Task VerifyRekorLinkage_ValidLinkage_ReturnsSuccess()
{
// Arrange
var observation = CreateTestObservation("obs-003") with
{
RekorUuid = "valid-uuid-12345678",
RekorLogIndex = 12345,
RekorIntegratedTime = FixedTimestamp.AddMinutes(-5),
RekorInclusionProof = CreateTestInclusionProof()
};
await _observationStore.UpsertAsync(observation, CancellationToken.None);
_rekorClient.SetupValidEntry("valid-uuid-12345678", 12345);
var service = CreateService();
// Act
var result = await service.VerifyRekorLinkageAsync("default", "obs-003", CancellationToken.None);
// Assert
result.IsVerified.Should().BeTrue();
result.InclusionProofValid.Should().BeTrue();
result.SignatureValid.Should().BeTrue();
}
[Fact]
public async Task VerifyRekorLinkage_NoLinkage_ReturnsNotLinked()
{
// Arrange
var observation = CreateTestObservation("obs-004");
await _observationStore.InsertAsync(observation, CancellationToken.None);
var service = CreateService();
// Act
var result = await service.VerifyRekorLinkageAsync("default", "obs-004", CancellationToken.None);
// Assert
result.IsVerified.Should().BeFalse();
result.FailureReason.Should().Contain("not linked");
}
[Fact]
public async Task VerifyRekorLinkage_Offline_UsesStoredProof()
{
// Arrange
var observation = CreateTestObservation("obs-005") with
{
RekorUuid = "offline-uuid-12345678",
RekorLogIndex = 12346,
RekorIntegratedTime = FixedTimestamp.AddMinutes(-10),
RekorInclusionProof = CreateTestInclusionProof()
};
await _observationStore.UpsertAsync(observation, CancellationToken.None);
// Disconnect Rekor (simulate offline)
_rekorClient.SetOffline(true);
var service = CreateService();
// Act
var result = await service.VerifyRekorLinkageAsync(
"default", "obs-005",
verifyOnline: false,
var result = await _mockAttestationService.Object.AttestAndLinkAsync(
observation,
new VexAttestationOptions { SubmitToRekor = true },
CancellationToken.None);
// Assert
result.IsVerified.Should().BeTrue();
result.VerificationMode.Should().Be("offline");
result.Success.Should().BeTrue();
result.RekorLinkage.Should().NotBeNull();
result.RekorLinkage!.Uuid.Should().Be("test-uuid-12345678");
result.RekorLinkage.LogIndex.Should().Be(12345);
}
[Fact]
public async Task AttestBatch_MultipleObservations_AttestsAll()
public async Task AttestAndLinkAsync_WhenObservationNotFound_ReturnsFailure()
{
// Arrange
var observations = Enumerable.Range(1, 5)
.Select(i => CreateTestObservation($"batch-obs-{i:D3}"))
.ToList();
var observation = CreateTestObservation("nonexistent");
var expectedResult = VexObservationAttestationResult.Failed(
"nonexistent",
"Observation not found",
VexAttestationErrorCode.ObservationNotFound);
foreach (var obs in observations)
{
await _observationStore.InsertAsync(obs, CancellationToken.None);
}
var service = CreateService();
var ids = observations.Select(o => o.ObservationId).ToList();
_mockAttestationService
.Setup(s => s.AttestAndLinkAsync(
It.IsAny<VexObservation>(),
It.IsAny<VexAttestationOptions>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(expectedResult);
// Act
var results = await service.AttestBatchAsync("default", ids, CancellationToken.None);
var result = await _mockAttestationService.Object.AttestAndLinkAsync(
observation,
new VexAttestationOptions { SubmitToRekor = true },
CancellationToken.None);
// Assert
results.TotalCount.Should().Be(5);
results.SuccessCount.Should().Be(5);
results.FailureCount.Should().Be(0);
result.Success.Should().BeFalse();
result.ErrorCode.Should().Be(VexAttestationErrorCode.ObservationNotFound);
}
[Fact]
public async Task GetPendingAttestations_ReturnsUnlinkedObservations()
public async Task VerifyLinkageAsync_WhenValid_ReturnsSuccess()
{
// Arrange
var linkedObs = CreateTestObservation("linked-001") with
var expectedLinkage = new RekorLinkage
{
RekorUuid = "already-linked",
RekorLogIndex = 100
Uuid = "valid-uuid-12345678",
LogIndex = 12345,
IntegratedTime = FixedTimestamp.AddMinutes(-5),
LogUrl = "https://rekor.sigstore.dev"
};
var unlinkedObs1 = CreateTestObservation("unlinked-001");
var unlinkedObs2 = CreateTestObservation("unlinked-002");
await _observationStore.UpsertAsync(linkedObs, CancellationToken.None);
await _observationStore.InsertAsync(unlinkedObs1, CancellationToken.None);
await _observationStore.InsertAsync(unlinkedObs2, CancellationToken.None);
var expectedResult = new RekorLinkageVerificationResult
{
IsValid = true,
Status = RekorLinkageVerificationStatus.Valid,
Linkage = expectedLinkage,
Message = null
};
var service = CreateService();
_mockAttestationService
.Setup(s => s.VerifyLinkageAsync(
It.Is<string>(id => id == "obs-003"),
It.IsAny<CancellationToken>()))
.ReturnsAsync(expectedResult);
// Act
var pending = await service.GetPendingAttestationsAsync("default", 10, CancellationToken.None);
var result = await _mockAttestationService.Object.VerifyLinkageAsync("obs-003", CancellationToken.None);
// Assert
result.IsValid.Should().BeTrue();
result.Linkage.Should().NotBeNull();
result.Linkage!.Uuid.Should().Be("valid-uuid-12345678");
}
[Fact]
public async Task VerifyLinkageAsync_WhenNoLinkage_ReturnsNotLinked()
{
// Arrange
_mockAttestationService
.Setup(s => s.VerifyLinkageAsync(
It.Is<string>(id => id == "obs-004"),
It.IsAny<CancellationToken>()))
.ReturnsAsync(RekorLinkageVerificationResult.NoLinkage);
// Act
var result = await _mockAttestationService.Object.VerifyLinkageAsync("obs-004", CancellationToken.None);
// Assert
result.IsValid.Should().BeFalse();
result.Status.Should().Be(RekorLinkageVerificationStatus.NoLinkage);
}
[Fact]
public async Task AttestBatchAsync_MultipleObservations_AttestsAll()
{
// Arrange
var ids = new List<string> { "batch-obs-001", "batch-obs-002", "batch-obs-003" };
var results = ids.Select(id => VexObservationAttestationResult.Succeeded(
id,
new RekorLinkage
{
Uuid = $"uuid-{id}",
LogIndex = 10000 + ids.IndexOf(id),
IntegratedTime = FixedTimestamp
})).ToList();
_mockAttestationService
.Setup(s => s.AttestBatchAsync(
It.IsAny<IReadOnlyList<string>>(),
It.IsAny<VexAttestationOptions>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(results);
// Act
var batchResults = await _mockAttestationService.Object.AttestBatchAsync(
ids,
new VexAttestationOptions { SubmitToRekor = true },
CancellationToken.None);
// Assert
batchResults.Should().HaveCount(3);
batchResults.All(r => r.Success).Should().BeTrue();
}
[Fact]
public async Task GetPendingAttestationsAsync_ReturnsUnlinkedObservationIds()
{
// Arrange
var pendingIds = new List<string> { "unlinked-001", "unlinked-002" };
_mockAttestationService
.Setup(s => s.GetPendingAttestationsAsync(
It.IsAny<int>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(pendingIds);
// Act
var pending = await _mockAttestationService.Object.GetPendingAttestationsAsync(10, CancellationToken.None);
// Assert
pending.Should().HaveCount(2);
pending.Select(p => p.ObservationId).Should().Contain("unlinked-001");
pending.Select(p => p.ObservationId).Should().Contain("unlinked-002");
pending.Select(p => p.ObservationId).Should().NotContain("linked-001");
}
[Fact]
public async Task AttestObservation_StoresInclusionProof()
{
// Arrange
var observation = CreateTestObservation("obs-proof-001");
await _observationStore.InsertAsync(observation, CancellationToken.None);
var service = CreateService(storeInclusionProof: true);
// Act
var result = await service.AttestAsync("default", "obs-proof-001", CancellationToken.None);
// Assert
result.Success.Should().BeTrue();
var updated = await _observationStore.GetByIdAsync("default", "obs-proof-001", CancellationToken.None);
updated!.RekorInclusionProof.Should().NotBeNull();
updated.RekorInclusionProof!.Hashes.Should().NotBeEmpty();
}
[Fact]
public async Task VerifyRekorLinkage_TamperedEntry_DetectsInconsistency()
{
// Arrange
var observation = CreateTestObservation("obs-tampered") with
{
RekorUuid = "tampered-uuid",
RekorLogIndex = 12347,
RekorIntegratedTime = FixedTimestamp.AddMinutes(-5)
};
await _observationStore.UpsertAsync(observation, CancellationToken.None);
// Setup Rekor to return different data than what was stored
_rekorClient.SetupTamperedEntry("tampered-uuid", 12347);
var service = CreateService();
// Act
var result = await service.VerifyRekorLinkageAsync("default", "obs-tampered", CancellationToken.None);
// Assert
result.IsVerified.Should().BeFalse();
result.FailureReason.Should().Contain("mismatch");
pending.Should().Contain("unlinked-001");
pending.Should().Contain("unlinked-002");
}
// Helper methods
private IVexObservationAttestationService CreateService(bool storeInclusionProof = false)
{
return new VexObservationAttestationService(
_observationStore,
_rekorClient,
Options.Create(new VexAttestationOptions
{
StoreInclusionProof = storeInclusionProof,
RekorUrl = "https://rekor.sigstore.dev"
}),
_timeProvider,
NullLogger<VexObservationAttestationService>.Instance);
}
private VexObservation CreateTestObservation(string id)
{
return new VexObservation(
@@ -286,212 +215,27 @@ public sealed class VexRekorAttestationFlowTests
providerId: "test-provider",
streamId: "test-stream",
upstream: new VexObservationUpstream(
url: "https://example.com/vex",
etag: "etag-123",
lastModified: FixedTimestamp.AddDays(-1),
format: "csaf",
fetchedAt: FixedTimestamp),
upstreamId: "upstream-1",
documentVersion: "1.0",
fetchedAt: FixedTimestamp.AddDays(-1),
receivedAt: FixedTimestamp,
contentHash: "sha256:abc123",
signature: new VexObservationSignature(false, null, null, null)),
statements: ImmutableArray.Create(
new VexObservationStatement(
vulnerabilityId: "CVE-2026-0001",
productKey: "pkg:example/test@1.0",
status: "not_affected",
justification: "code_not_present",
actionStatement: null,
impact: null,
timestamp: FixedTimestamp.AddDays(-1))),
status: VexClaimStatus.NotAffected,
lastObserved: FixedTimestamp.AddDays(-1))),
content: new VexObservationContent(
raw: """{"test": "content"}""",
mediaType: "application/json",
encoding: "utf-8",
signature: null),
format: "csaf",
specVersion: "2.0",
raw: JsonNode.Parse("""{"test": "content"}""")!),
linkset: new VexObservationLinkset(
advisoryLinks: ImmutableArray<VexObservationReference>.Empty,
productLinks: ImmutableArray<VexObservationReference>.Empty,
vulnerabilityLinks: ImmutableArray<VexObservationReference>.Empty),
aliases: null,
purls: null,
cpes: null,
references: null),
createdAt: FixedTimestamp);
}
private static VexInclusionProof CreateTestInclusionProof()
{
return new VexInclusionProof(
TreeSize: 100000,
RootHash: "dGVzdC1yb290LWhhc2g=",
LogIndex: 12345,
Hashes: ImmutableArray.Create(
"aGFzaDE=",
"aGFzaDI=",
"aGFzaDM="));
}
}
// Supporting types for tests
public record VexInclusionProof(
long TreeSize,
string RootHash,
long LogIndex,
ImmutableArray<string> Hashes);
public sealed class InMemoryVexObservationStore : IVexObservationStore
{
private readonly Dictionary<(string Tenant, string Id), VexObservation> _store = new();
public ValueTask<bool> InsertAsync(VexObservation observation, CancellationToken ct)
{
var key = (observation.Tenant, observation.ObservationId);
if (_store.ContainsKey(key)) return ValueTask.FromResult(false);
_store[key] = observation;
return ValueTask.FromResult(true);
}
public ValueTask<bool> UpsertAsync(VexObservation observation, CancellationToken ct)
{
var key = (observation.Tenant, observation.ObservationId);
_store[key] = observation;
return ValueTask.FromResult(true);
}
public ValueTask<int> InsertManyAsync(string tenant, IEnumerable<VexObservation> observations, CancellationToken ct)
{
var count = 0;
foreach (var obs in observations.Where(o => o.Tenant == tenant))
{
var key = (obs.Tenant, obs.ObservationId);
if (!_store.ContainsKey(key))
{
_store[key] = obs;
count++;
}
}
return ValueTask.FromResult(count);
}
public ValueTask<VexObservation?> GetByIdAsync(string tenant, string observationId, CancellationToken ct)
{
_store.TryGetValue((tenant, observationId), out var obs);
return ValueTask.FromResult(obs);
}
public ValueTask<IReadOnlyList<VexObservation>> FindByVulnerabilityAndProductAsync(
string tenant, string vulnerabilityId, string productKey, CancellationToken ct)
{
var results = _store.Values
.Where(o => o.Tenant == tenant)
.Where(o => o.Statements.Any(s => s.VulnerabilityId == vulnerabilityId && s.ProductKey == productKey))
.ToList();
return ValueTask.FromResult<IReadOnlyList<VexObservation>>(results);
}
public ValueTask<IReadOnlyList<VexObservation>> FindByProviderAsync(
string tenant, string providerId, int limit, CancellationToken ct)
{
var results = _store.Values
.Where(o => o.Tenant == tenant && o.ProviderId == providerId)
.Take(limit)
.ToList();
return ValueTask.FromResult<IReadOnlyList<VexObservation>>(results);
}
public ValueTask<bool> DeleteAsync(string tenant, string observationId, CancellationToken ct)
{
return ValueTask.FromResult(_store.Remove((tenant, observationId)));
}
public ValueTask<long> CountAsync(string tenant, CancellationToken ct)
{
var count = _store.Values.Count(o => o.Tenant == tenant);
return ValueTask.FromResult((long)count);
}
public ValueTask<bool> UpdateRekorLinkageAsync(
string tenant, string observationId, RekorLinkage linkage, CancellationToken ct)
{
if (!_store.TryGetValue((tenant, observationId), out var obs))
return ValueTask.FromResult(false);
_store[(tenant, observationId)] = obs with
{
RekorUuid = linkage.EntryUuid,
RekorLogIndex = linkage.LogIndex,
RekorIntegratedTime = linkage.IntegratedTime,
RekorLogUrl = linkage.LogUrl
};
return ValueTask.FromResult(true);
}
public ValueTask<IReadOnlyList<VexObservation>> GetPendingRekorAttestationAsync(
string tenant, int limit, CancellationToken ct)
{
var results = _store.Values
.Where(o => o.Tenant == tenant && string.IsNullOrEmpty(o.RekorUuid))
.Take(limit)
.ToList();
return ValueTask.FromResult<IReadOnlyList<VexObservation>>(results);
}
public ValueTask<VexObservation?> GetByRekorUuidAsync(string tenant, string rekorUuid, CancellationToken ct)
{
var obs = _store.Values.FirstOrDefault(o => o.Tenant == tenant && o.RekorUuid == rekorUuid);
return ValueTask.FromResult(obs);
}
}
public sealed class MockRekorClient
{
private readonly Dictionary<string, (long LogIndex, bool Valid, bool Tampered)> _entries = new();
private bool _offline;
private long _nextLogIndex = 10000;
public void SetupValidEntry(string uuid, long logIndex)
{
_entries[uuid] = (logIndex, true, false);
}
public void SetupTamperedEntry(string uuid, long logIndex)
{
_entries[uuid] = (logIndex, false, true);
}
public void SetOffline(bool offline)
{
_offline = offline;
}
public Task<RekorSubmitResult> SubmitAsync(byte[] payload, CancellationToken ct)
{
if (_offline)
{
return Task.FromResult(new RekorSubmitResult(false, null, 0, "offline"));
}
var uuid = Guid.NewGuid().ToString("N");
var logIndex = _nextLogIndex++;
_entries[uuid] = (logIndex, true, false);
return Task.FromResult(new RekorSubmitResult(true, uuid, logIndex, null));
}
public Task<RekorVerifyResult> VerifyAsync(string uuid, CancellationToken ct)
{
if (_offline)
{
return Task.FromResult(new RekorVerifyResult(false, "offline", null, null));
}
if (_entries.TryGetValue(uuid, out var entry))
{
if (entry.Tampered)
{
return Task.FromResult(new RekorVerifyResult(false, "hash mismatch", null, null));
}
return Task.FromResult(new RekorVerifyResult(true, null, true, true));
}
return Task.FromResult(new RekorVerifyResult(false, "entry not found", null, null));
}
}
public record RekorSubmitResult(bool Success, string? EntryId, long LogIndex, string? Error);
public record RekorVerifyResult(bool IsVerified, string? FailureReason, bool? SignatureValid, bool? InclusionProofValid);

View File

@@ -77,19 +77,18 @@ public sealed class VexStatementChangeEventTests
tenant: "default",
vulnerabilityId: "CVE-2026-1234",
productKey: "pkg:npm/lodash@4.17.21",
status: "fixed",
newStatus: "fixed",
previousStatus: "not_affected",
providerId: "vendor:redhat",
observationId: "default:redhat:VEX-2026-0001:v2",
supersedes: ImmutableArray.Create("default:redhat:VEX-2026-0001:v1"),
supersededBy: "default:redhat:VEX-2026-0001:v1",
occurredAtUtc: FixedTimestamp);
// Assert
Assert.Equal(VexTimelineEventTypes.StatementSuperseded, evt.EventType);
Assert.Equal("fixed", evt.NewStatus);
Assert.Equal("not_affected", evt.PreviousStatus);
Assert.Single(evt.Supersedes);
Assert.Equal("default:redhat:VEX-2026-0001:v1", evt.Supersedes[0]);
Assert.Equal("default:redhat:VEX-2026-0001:v1", evt.SupersededBy);
}
[Fact]
@@ -112,13 +111,21 @@ public sealed class VexStatementChangeEventTests
TrustScore = 0.85
});
var conflictDetails = new VexConflictDetails
{
ConflictType = "status_mismatch",
ConflictingStatuses = conflictingStatuses,
AutoResolved = false
};
// Act
var evt = VexStatementChangeEventFactory.CreateConflictDetected(
tenant: "default",
vulnerabilityId: "CVE-2026-1234",
productKey: "pkg:npm/lodash@4.17.21",
conflictType: "status_mismatch",
conflictingStatuses: conflictingStatuses,
providerId: "vendor:redhat",
observationId: "default:redhat:VEX-2026-0001",
conflictDetails: conflictDetails,
occurredAtUtc: FixedTimestamp);
// Assert
@@ -149,13 +156,21 @@ public sealed class VexStatementChangeEventTests
TrustScore = 0.95
});
var conflictDetails = new VexConflictDetails
{
ConflictType = "status_mismatch",
ConflictingStatuses = conflictingStatuses,
AutoResolved = false
};
// Act
var evt = VexStatementChangeEventFactory.CreateConflictDetected(
tenant: "default",
vulnerabilityId: "CVE-2026-1234",
productKey: "pkg:npm/lodash@4.17.21",
conflictType: "status_mismatch",
conflictingStatuses: conflictingStatuses,
providerId: "vendor:redhat",
observationId: "default:redhat:VEX-2026-0001",
conflictDetails: conflictDetails,
occurredAtUtc: FixedTimestamp);
// Assert - Should be sorted by provider ID for determinism
@@ -200,10 +215,10 @@ public sealed class VexStatementChangeEventTests
}
[Fact]
public void CreateStatusChanged_TracksStatusTransition()
public void CreateStatementSuperseded_TracksStatusTransition()
{
// Arrange & Act
var evt = VexStatementChangeEventFactory.CreateStatusChanged(
var evt = VexStatementChangeEventFactory.CreateStatementSuperseded(
tenant: "default",
vulnerabilityId: "CVE-2026-1234",
productKey: "pkg:npm/lodash@4.17.21",
@@ -211,10 +226,11 @@ public sealed class VexStatementChangeEventTests
previousStatus: "affected",
providerId: "vendor:redhat",
observationId: "default:redhat:VEX-2026-0001:v3",
supersededBy: "default:redhat:VEX-2026-0001:v2",
occurredAtUtc: FixedTimestamp);
// Assert
Assert.Equal(VexTimelineEventTypes.StatusChanged, evt.EventType);
Assert.Equal(VexTimelineEventTypes.StatementSuperseded, evt.EventType);
Assert.Equal("fixed", evt.NewStatus);
Assert.Equal("affected", evt.PreviousStatus);
}