sprints work

This commit is contained in:
StellaOps Bot
2025-12-24 21:46:08 +02:00
parent 43e2af88f6
commit b9f71fc7e9
161 changed files with 29566 additions and 527 deletions

View File

@@ -0,0 +1,287 @@
using System.Text;
using System.Text.Json;
using Xunit;
namespace StellaOps.Evidence.Core.Tests;
/// <summary>
/// Unit tests for EvidenceRecord creation and ID computation.
/// </summary>
public class EvidenceRecordTests
{
private static readonly EvidenceProvenance TestProvenance = new()
{
GeneratorId = "stellaops/test/unit",
GeneratorVersion = "1.0.0",
GeneratedAt = new DateTimeOffset(2025, 12, 24, 12, 0, 0, TimeSpan.Zero)
};
#region ComputeEvidenceId
[Fact]
public void ComputeEvidenceId_ValidInputs_ReturnsSha256Prefixed()
{
var subjectId = "sha256:abc123";
var payload = Encoding.UTF8.GetBytes("""{"vulnerability":"CVE-2021-44228"}""");
var evidenceId = EvidenceRecord.ComputeEvidenceId(
subjectId,
EvidenceType.Scan,
payload,
TestProvenance);
Assert.StartsWith("sha256:", evidenceId);
Assert.Equal(71, evidenceId.Length); // "sha256:" + 64 hex chars
}
[Fact]
public void ComputeEvidenceId_SameInputs_ReturnsSameId()
{
var subjectId = "sha256:abc123";
var payload = Encoding.UTF8.GetBytes("""{"vulnerability":"CVE-2021-44228"}""");
var id1 = EvidenceRecord.ComputeEvidenceId(subjectId, EvidenceType.Scan, payload, TestProvenance);
var id2 = EvidenceRecord.ComputeEvidenceId(subjectId, EvidenceType.Scan, payload, TestProvenance);
Assert.Equal(id1, id2);
}
[Fact]
public void ComputeEvidenceId_DifferentSubjects_ReturnsDifferentIds()
{
var payload = Encoding.UTF8.GetBytes("""{"vulnerability":"CVE-2021-44228"}""");
var id1 = EvidenceRecord.ComputeEvidenceId("sha256:abc123", EvidenceType.Scan, payload, TestProvenance);
var id2 = EvidenceRecord.ComputeEvidenceId("sha256:def456", EvidenceType.Scan, payload, TestProvenance);
Assert.NotEqual(id1, id2);
}
[Fact]
public void ComputeEvidenceId_DifferentTypes_ReturnsDifferentIds()
{
var subjectId = "sha256:abc123";
var payload = Encoding.UTF8.GetBytes("""{"data":"test"}""");
var id1 = EvidenceRecord.ComputeEvidenceId(subjectId, EvidenceType.Scan, payload, TestProvenance);
var id2 = EvidenceRecord.ComputeEvidenceId(subjectId, EvidenceType.Vex, payload, TestProvenance);
Assert.NotEqual(id1, id2);
}
[Fact]
public void ComputeEvidenceId_DifferentPayloads_ReturnsDifferentIds()
{
var subjectId = "sha256:abc123";
var payload1 = Encoding.UTF8.GetBytes("""{"vulnerability":"CVE-2021-44228"}""");
var payload2 = Encoding.UTF8.GetBytes("""{"vulnerability":"CVE-2021-45046"}""");
var id1 = EvidenceRecord.ComputeEvidenceId(subjectId, EvidenceType.Scan, payload1, TestProvenance);
var id2 = EvidenceRecord.ComputeEvidenceId(subjectId, EvidenceType.Scan, payload2, TestProvenance);
Assert.NotEqual(id1, id2);
}
[Fact]
public void ComputeEvidenceId_DifferentProvenance_ReturnsDifferentIds()
{
var subjectId = "sha256:abc123";
var payload = Encoding.UTF8.GetBytes("""{"vulnerability":"CVE-2021-44228"}""");
var prov1 = new EvidenceProvenance
{
GeneratorId = "stellaops/scanner/trivy",
GeneratorVersion = "1.0.0",
GeneratedAt = new DateTimeOffset(2025, 12, 24, 12, 0, 0, TimeSpan.Zero)
};
var prov2 = new EvidenceProvenance
{
GeneratorId = "stellaops/scanner/grype",
GeneratorVersion = "1.0.0",
GeneratedAt = new DateTimeOffset(2025, 12, 24, 12, 0, 0, TimeSpan.Zero)
};
var id1 = EvidenceRecord.ComputeEvidenceId(subjectId, EvidenceType.Scan, payload, prov1);
var id2 = EvidenceRecord.ComputeEvidenceId(subjectId, EvidenceType.Scan, payload, prov2);
Assert.NotEqual(id1, id2);
}
[Fact]
public void ComputeEvidenceId_NullSubject_ThrowsArgumentException()
{
var payload = Encoding.UTF8.GetBytes("""{"data":"test"}""");
Assert.ThrowsAny<ArgumentException>(() =>
EvidenceRecord.ComputeEvidenceId(null!, EvidenceType.Scan, payload, TestProvenance));
}
[Fact]
public void ComputeEvidenceId_EmptySubject_ThrowsArgumentException()
{
var payload = Encoding.UTF8.GetBytes("""{"data":"test"}""");
Assert.ThrowsAny<ArgumentException>(() =>
EvidenceRecord.ComputeEvidenceId("", EvidenceType.Scan, payload, TestProvenance));
}
[Fact]
public void ComputeEvidenceId_NullProvenance_ThrowsArgumentNullException()
{
var payload = Encoding.UTF8.GetBytes("""{"data":"test"}""");
Assert.Throws<ArgumentNullException>(() =>
EvidenceRecord.ComputeEvidenceId("sha256:abc", EvidenceType.Scan, payload, null!));
}
#endregion
#region Create Factory Method
[Fact]
public void Create_ValidInputs_ReturnsRecordWithComputedId()
{
var subjectId = "sha256:abc123";
var payload = Encoding.UTF8.GetBytes("""{"vulnerability":"CVE-2021-44228"}""");
var record = EvidenceRecord.Create(
subjectId,
EvidenceType.Scan,
payload,
TestProvenance,
"scan/v1");
Assert.Equal(subjectId, record.SubjectNodeId);
Assert.Equal(EvidenceType.Scan, record.EvidenceType);
Assert.StartsWith("sha256:", record.EvidenceId);
Assert.Equal("scan/v1", record.PayloadSchemaVersion);
Assert.Equal(TestProvenance, record.Provenance);
Assert.Empty(record.Signatures);
Assert.Null(record.ExternalPayloadCid);
}
[Fact]
public void Create_WithSignatures_IncludesSignatures()
{
var subjectId = "sha256:abc123";
var payload = Encoding.UTF8.GetBytes("""{"data":"test"}""");
var signature = new EvidenceSignature
{
SignerId = "key-123",
Algorithm = "ES256",
SignatureBase64 = "MEUCIQC...",
SignedAt = DateTimeOffset.UtcNow
};
var record = EvidenceRecord.Create(
subjectId,
EvidenceType.Scan,
payload,
TestProvenance,
"scan/v1",
signatures: [signature]);
Assert.Single(record.Signatures);
Assert.Equal("key-123", record.Signatures[0].SignerId);
}
[Fact]
public void Create_WithExternalCid_IncludesCid()
{
var subjectId = "sha256:abc123";
var payload = Array.Empty<byte>(); // Empty when using external CID
var record = EvidenceRecord.Create(
subjectId,
EvidenceType.Reachability,
payload,
TestProvenance,
"reachability/v1",
externalPayloadCid: "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi");
Assert.Equal("bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", record.ExternalPayloadCid);
}
#endregion
#region VerifyIntegrity
[Fact]
public void VerifyIntegrity_ValidRecord_ReturnsTrue()
{
var record = EvidenceRecord.Create(
"sha256:abc123",
EvidenceType.Scan,
Encoding.UTF8.GetBytes("""{"vulnerability":"CVE-2021-44228"}"""),
TestProvenance,
"scan/v1");
Assert.True(record.VerifyIntegrity());
}
[Fact]
public void VerifyIntegrity_TamperedPayload_ReturnsFalse()
{
var originalPayload = Encoding.UTF8.GetBytes("""{"vulnerability":"CVE-2021-44228"}""");
var tamperedPayload = Encoding.UTF8.GetBytes("""{"vulnerability":"CVE-TAMPERED"}""");
var record = EvidenceRecord.Create(
"sha256:abc123",
EvidenceType.Scan,
originalPayload,
TestProvenance,
"scan/v1");
// Create a tampered record with the original ID but different payload
var tampered = record with { Payload = tamperedPayload };
Assert.False(tampered.VerifyIntegrity());
}
[Fact]
public void VerifyIntegrity_TamperedSubject_ReturnsFalse()
{
var record = EvidenceRecord.Create(
"sha256:abc123",
EvidenceType.Scan,
Encoding.UTF8.GetBytes("""{"vulnerability":"CVE-2021-44228"}"""),
TestProvenance,
"scan/v1");
var tampered = record with { SubjectNodeId = "sha256:tampered" };
Assert.False(tampered.VerifyIntegrity());
}
#endregion
#region Determinism
[Fact]
public void Create_SameInputs_ProducesSameEvidenceId()
{
var subjectId = "sha256:abc123";
var payload = Encoding.UTF8.GetBytes("""{"vulnerability":"CVE-2021-44228","severity":"critical"}""");
var ids = Enumerable.Range(0, 100)
.Select(_ => EvidenceRecord.Create(subjectId, EvidenceType.Scan, payload, TestProvenance, "scan/v1"))
.Select(r => r.EvidenceId)
.Distinct()
.ToList();
Assert.Single(ids);
}
[Fact]
public void ComputeEvidenceId_EmptyPayload_Works()
{
var id = EvidenceRecord.ComputeEvidenceId(
"sha256:abc123",
EvidenceType.Artifact,
[],
TestProvenance);
Assert.StartsWith("sha256:", id);
}
#endregion
}

View File

@@ -0,0 +1,287 @@
// <copyright file="ExceptionApplicationAdapterTests.cs" company="StellaOps">
// SPDX-License-Identifier: AGPL-3.0-or-later
// </copyright>
using System.Collections.Immutable;
using StellaOps.Evidence.Core;
using StellaOps.Evidence.Core.Adapters;
namespace StellaOps.Evidence.Core.Tests;
public sealed class ExceptionApplicationAdapterTests
{
private readonly ExceptionApplicationAdapter _adapter = new();
private readonly string _subjectNodeId = "sha256:finding123";
private readonly EvidenceProvenance _provenance;
public ExceptionApplicationAdapterTests()
{
_provenance = new EvidenceProvenance
{
GeneratorId = "policy-engine",
GeneratorVersion = "2.0.0",
GeneratedAt = DateTimeOffset.Parse("2025-01-15T12:00:00Z")
};
}
[Fact]
public void CanConvert_WithValidApplication_ReturnsTrue()
{
var application = CreateValidApplication();
var result = _adapter.CanConvert(application);
Assert.True(result);
}
[Fact]
public void CanConvert_WithNullApplication_ReturnsFalse()
{
var result = _adapter.CanConvert(null!);
Assert.False(result);
}
[Fact]
public void CanConvert_WithEmptyExceptionId_ReturnsFalse()
{
var application = CreateValidApplication() with { ExceptionId = "" };
var result = _adapter.CanConvert(application);
Assert.False(result);
}
[Fact]
public void CanConvert_WithEmptyFindingId_ReturnsFalse()
{
var application = CreateValidApplication() with { FindingId = "" };
var result = _adapter.CanConvert(application);
Assert.False(result);
}
[Fact]
public void Convert_CreatesSingleRecord()
{
var application = CreateValidApplication();
var results = _adapter.Convert(application, _subjectNodeId, _provenance);
Assert.Single(results);
}
[Fact]
public void Convert_RecordHasExceptionType()
{
var application = CreateValidApplication();
var results = _adapter.Convert(application, _subjectNodeId, _provenance);
Assert.Equal(EvidenceType.Exception, results[0].EvidenceType);
}
[Fact]
public void Convert_RecordHasCorrectSubjectNodeId()
{
var application = CreateValidApplication();
var results = _adapter.Convert(application, _subjectNodeId, _provenance);
Assert.Equal(_subjectNodeId, results[0].SubjectNodeId);
}
[Fact]
public void Convert_RecordHasNonEmptyPayload()
{
var application = CreateValidApplication();
var results = _adapter.Convert(application, _subjectNodeId, _provenance);
Assert.False(results[0].Payload.IsEmpty);
}
[Fact]
public void Convert_RecordHasPayloadSchemaVersion()
{
var application = CreateValidApplication();
var results = _adapter.Convert(application, _subjectNodeId, _provenance);
Assert.Equal("1.0.0", results[0].PayloadSchemaVersion);
}
[Fact]
public void Convert_RecordHasEmptySignatures()
{
var application = CreateValidApplication();
var results = _adapter.Convert(application, _subjectNodeId, _provenance);
Assert.Empty(results[0].Signatures);
}
[Fact]
public void Convert_UsesProvidedProvenance()
{
var application = CreateValidApplication();
var results = _adapter.Convert(application, _subjectNodeId, _provenance);
Assert.Equal(_provenance.GeneratorId, results[0].Provenance.GeneratorId);
Assert.Equal(_provenance.GeneratorVersion, results[0].Provenance.GeneratorVersion);
}
[Fact]
public void Convert_RecordHasUniqueEvidenceId()
{
var application = CreateValidApplication();
var results = _adapter.Convert(application, _subjectNodeId, _provenance);
Assert.NotNull(results[0].EvidenceId);
Assert.NotEmpty(results[0].EvidenceId);
}
[Fact]
public void Convert_WithNullSubjectNodeId_ThrowsArgumentNullException()
{
var application = CreateValidApplication();
Assert.Throws<ArgumentNullException>(() =>
_adapter.Convert(application, null!, _provenance));
}
[Fact]
public void Convert_WithEmptySubjectNodeId_ThrowsArgumentException()
{
var application = CreateValidApplication();
Assert.Throws<ArgumentException>(() =>
_adapter.Convert(application, "", _provenance));
}
[Fact]
public void Convert_WithNullProvenance_ThrowsArgumentNullException()
{
var application = CreateValidApplication();
Assert.Throws<ArgumentNullException>(() =>
_adapter.Convert(application, _subjectNodeId, null!));
}
[Fact]
public void Convert_WithVulnerabilityId_IncludesInPayload()
{
var application = CreateValidApplication() with { VulnerabilityId = "CVE-2024-9999" };
var results = _adapter.Convert(application, _subjectNodeId, _provenance);
Assert.False(results[0].Payload.IsEmpty);
}
[Fact]
public void Convert_WithEvaluationRunId_IncludesInPayload()
{
var runId = Guid.NewGuid();
var application = CreateValidApplication() with { EvaluationRunId = runId };
var results = _adapter.Convert(application, _subjectNodeId, _provenance);
Assert.False(results[0].Payload.IsEmpty);
}
[Fact]
public void Convert_WithPolicyBundleDigest_IncludesInPayload()
{
var application = CreateValidApplication() with { PolicyBundleDigest = "sha256:policy123" };
var results = _adapter.Convert(application, _subjectNodeId, _provenance);
Assert.False(results[0].Payload.IsEmpty);
}
[Fact]
public void Convert_WithMetadata_IncludesInPayload()
{
var metadata = ImmutableDictionary<string, string>.Empty
.Add("key1", "value1")
.Add("key2", "value2");
var application = CreateValidApplication() with { Metadata = metadata };
var results = _adapter.Convert(application, _subjectNodeId, _provenance);
Assert.False(results[0].Payload.IsEmpty);
}
[Fact]
public void Convert_DifferentApplications_ProduceDifferentEvidenceIds()
{
var app1 = CreateValidApplication() with { ExceptionId = "exc-001" };
var app2 = CreateValidApplication() with { ExceptionId = "exc-002" };
var results1 = _adapter.Convert(app1, _subjectNodeId, _provenance);
var results2 = _adapter.Convert(app2, _subjectNodeId, _provenance);
Assert.NotEqual(results1[0].EvidenceId, results2[0].EvidenceId);
}
[Fact]
public void Convert_SameApplicationTwice_ProducesSameEvidenceId()
{
var application = CreateValidApplication();
var results1 = _adapter.Convert(application, _subjectNodeId, _provenance);
var results2 = _adapter.Convert(application, _subjectNodeId, _provenance);
Assert.Equal(results1[0].EvidenceId, results2[0].EvidenceId);
}
[Fact]
public void Convert_AllStatusTransitions_Supported()
{
var transitions = new[]
{
("affected", "not_affected"),
("not_affected", "affected"),
("under_investigation", "fixed"),
("affected", "suppressed")
};
foreach (var (original, applied) in transitions)
{
var application = CreateValidApplication() with
{
OriginalStatus = original,
AppliedStatus = applied
};
var results = _adapter.Convert(application, _subjectNodeId, _provenance);
Assert.Single(results);
Assert.Equal(EvidenceType.Exception, results[0].EvidenceType);
}
}
private ExceptionApplicationInput CreateValidApplication()
{
return new ExceptionApplicationInput
{
Id = Guid.NewGuid(),
TenantId = Guid.NewGuid(),
ExceptionId = "exc-default",
FindingId = "finding-001",
VulnerabilityId = null,
OriginalStatus = "affected",
AppliedStatus = "not_affected",
EffectName = "suppress",
EffectType = "suppress",
EvaluationRunId = null,
PolicyBundleDigest = null,
AppliedAt = DateTimeOffset.Parse("2025-01-15T11:00:00Z"),
Metadata = ImmutableDictionary<string, string>.Empty
};
}
}

View File

@@ -0,0 +1,355 @@
using System.Text;
using Xunit;
namespace StellaOps.Evidence.Core.Tests;
/// <summary>
/// Unit tests for InMemoryEvidenceStore.
/// </summary>
public class InMemoryEvidenceStoreTests
{
private readonly InMemoryEvidenceStore _store = new();
private static readonly EvidenceProvenance TestProvenance = new()
{
GeneratorId = "stellaops/test/unit",
GeneratorVersion = "1.0.0",
GeneratedAt = new DateTimeOffset(2025, 12, 24, 12, 0, 0, TimeSpan.Zero)
};
private static EvidenceRecord CreateTestEvidence(
string subjectId,
EvidenceType type = EvidenceType.Scan,
string? payloadContent = null)
{
var payload = Encoding.UTF8.GetBytes(payloadContent ?? """{"data":"test"}""");
return EvidenceRecord.Create(subjectId, type, payload, TestProvenance, $"{type.ToString().ToLowerInvariant()}/v1");
}
#region StoreAsync
[Fact]
public async Task StoreAsync_ValidEvidence_ReturnsEvidenceId()
{
var evidence = CreateTestEvidence("sha256:subject1");
var result = await _store.StoreAsync(evidence);
Assert.Equal(evidence.EvidenceId, result);
Assert.Equal(1, _store.Count);
}
[Fact]
public async Task StoreAsync_DuplicateEvidence_IsIdempotent()
{
var evidence = CreateTestEvidence("sha256:subject1");
await _store.StoreAsync(evidence);
await _store.StoreAsync(evidence);
Assert.Equal(1, _store.Count);
}
[Fact]
public async Task StoreAsync_NullEvidence_ThrowsArgumentNullException()
{
await Assert.ThrowsAsync<ArgumentNullException>(() => _store.StoreAsync(null!));
}
#endregion
#region StoreBatchAsync
[Fact]
public async Task StoreBatchAsync_MultipleRecords_StoresAll()
{
var evidence1 = CreateTestEvidence("sha256:subject1");
var evidence2 = CreateTestEvidence("sha256:subject2");
var evidence3 = CreateTestEvidence("sha256:subject3");
var count = await _store.StoreBatchAsync([evidence1, evidence2, evidence3]);
Assert.Equal(3, count);
Assert.Equal(3, _store.Count);
}
[Fact]
public async Task StoreBatchAsync_WithDuplicates_SkipsDuplicates()
{
var evidence1 = CreateTestEvidence("sha256:subject1");
var evidence2 = CreateTestEvidence("sha256:subject2");
await _store.StoreAsync(evidence1);
var count = await _store.StoreBatchAsync([evidence1, evidence2]);
Assert.Equal(1, count); // Only evidence2 was new
Assert.Equal(2, _store.Count);
}
[Fact]
public async Task StoreBatchAsync_EmptyList_ReturnsZero()
{
var count = await _store.StoreBatchAsync([]);
Assert.Equal(0, count);
Assert.Equal(0, _store.Count);
}
#endregion
#region GetByIdAsync
[Fact]
public async Task GetByIdAsync_ExistingEvidence_ReturnsEvidence()
{
var evidence = CreateTestEvidence("sha256:subject1");
await _store.StoreAsync(evidence);
var result = await _store.GetByIdAsync(evidence.EvidenceId);
Assert.NotNull(result);
Assert.Equal(evidence.EvidenceId, result.EvidenceId);
Assert.Equal(evidence.SubjectNodeId, result.SubjectNodeId);
}
[Fact]
public async Task GetByIdAsync_NonExistingEvidence_ReturnsNull()
{
var result = await _store.GetByIdAsync("sha256:nonexistent");
Assert.Null(result);
}
[Fact]
public async Task GetByIdAsync_NullId_ThrowsArgumentException()
{
await Assert.ThrowsAnyAsync<ArgumentException>(() => _store.GetByIdAsync(null!));
}
[Fact]
public async Task GetByIdAsync_EmptyId_ThrowsArgumentException()
{
await Assert.ThrowsAnyAsync<ArgumentException>(() => _store.GetByIdAsync(""));
}
#endregion
#region GetBySubjectAsync
[Fact]
public async Task GetBySubjectAsync_ExistingSubject_ReturnsAllEvidence()
{
var subjectId = "sha256:subject1";
var evidence1 = CreateTestEvidence(subjectId, EvidenceType.Scan);
var evidence2 = CreateTestEvidence(subjectId, EvidenceType.Vex, """{"status":"not_affected"}""");
await _store.StoreAsync(evidence1);
await _store.StoreAsync(evidence2);
var results = await _store.GetBySubjectAsync(subjectId);
Assert.Equal(2, results.Count);
}
[Fact]
public async Task GetBySubjectAsync_WithTypeFilter_ReturnsFilteredResults()
{
var subjectId = "sha256:subject1";
var scanEvidence = CreateTestEvidence(subjectId, EvidenceType.Scan);
var vexEvidence = CreateTestEvidence(subjectId, EvidenceType.Vex, """{"status":"not_affected"}""");
await _store.StoreAsync(scanEvidence);
await _store.StoreAsync(vexEvidence);
var results = await _store.GetBySubjectAsync(subjectId, EvidenceType.Scan);
Assert.Single(results);
Assert.Equal(EvidenceType.Scan, results[0].EvidenceType);
}
[Fact]
public async Task GetBySubjectAsync_NonExistingSubject_ReturnsEmptyList()
{
var results = await _store.GetBySubjectAsync("sha256:nonexistent");
Assert.Empty(results);
}
#endregion
#region GetByTypeAsync
[Fact]
public async Task GetByTypeAsync_ExistingType_ReturnsMatchingEvidence()
{
await _store.StoreAsync(CreateTestEvidence("sha256:sub1", EvidenceType.Scan));
await _store.StoreAsync(CreateTestEvidence("sha256:sub2", EvidenceType.Scan));
await _store.StoreAsync(CreateTestEvidence("sha256:sub3", EvidenceType.Vex, """{"status":"affected"}"""));
var results = await _store.GetByTypeAsync(EvidenceType.Scan);
Assert.Equal(2, results.Count);
Assert.All(results, r => Assert.Equal(EvidenceType.Scan, r.EvidenceType));
}
[Fact]
public async Task GetByTypeAsync_WithLimit_RespectsLimit()
{
for (int i = 0; i < 10; i++)
{
await _store.StoreAsync(CreateTestEvidence($"sha256:sub{i}", EvidenceType.Scan, $"{{\"index\":{i}}}"));
}
var results = await _store.GetByTypeAsync(EvidenceType.Scan, limit: 5);
Assert.Equal(5, results.Count);
}
[Fact]
public async Task GetByTypeAsync_NonExistingType_ReturnsEmptyList()
{
await _store.StoreAsync(CreateTestEvidence("sha256:sub1", EvidenceType.Scan));
var results = await _store.GetByTypeAsync(EvidenceType.Kev);
Assert.Empty(results);
}
#endregion
#region ExistsAsync
[Fact]
public async Task ExistsAsync_ExistingEvidenceForType_ReturnsTrue()
{
var subjectId = "sha256:subject1";
await _store.StoreAsync(CreateTestEvidence(subjectId, EvidenceType.Scan));
var exists = await _store.ExistsAsync(subjectId, EvidenceType.Scan);
Assert.True(exists);
}
[Fact]
public async Task ExistsAsync_DifferentType_ReturnsFalse()
{
var subjectId = "sha256:subject1";
await _store.StoreAsync(CreateTestEvidence(subjectId, EvidenceType.Scan));
var exists = await _store.ExistsAsync(subjectId, EvidenceType.Vex);
Assert.False(exists);
}
[Fact]
public async Task ExistsAsync_NonExistingSubject_ReturnsFalse()
{
var exists = await _store.ExistsAsync("sha256:nonexistent", EvidenceType.Scan);
Assert.False(exists);
}
#endregion
#region DeleteAsync
[Fact]
public async Task DeleteAsync_ExistingEvidence_ReturnsTrue()
{
var evidence = CreateTestEvidence("sha256:subject1");
await _store.StoreAsync(evidence);
var deleted = await _store.DeleteAsync(evidence.EvidenceId);
Assert.True(deleted);
Assert.Equal(0, _store.Count);
}
[Fact]
public async Task DeleteAsync_NonExistingEvidence_ReturnsFalse()
{
var deleted = await _store.DeleteAsync("sha256:nonexistent");
Assert.False(deleted);
}
[Fact]
public async Task DeleteAsync_RemovedEvidence_NotRetrievable()
{
var evidence = CreateTestEvidence("sha256:subject1");
await _store.StoreAsync(evidence);
await _store.DeleteAsync(evidence.EvidenceId);
var result = await _store.GetByIdAsync(evidence.EvidenceId);
Assert.Null(result);
}
#endregion
#region CountBySubjectAsync
[Fact]
public async Task CountBySubjectAsync_MultipleEvidence_ReturnsCorrectCount()
{
var subjectId = "sha256:subject1";
await _store.StoreAsync(CreateTestEvidence(subjectId, EvidenceType.Scan));
await _store.StoreAsync(CreateTestEvidence(subjectId, EvidenceType.Vex, """{"status":"not_affected"}"""));
await _store.StoreAsync(CreateTestEvidence(subjectId, EvidenceType.Epss, """{"score":0.5}"""));
var count = await _store.CountBySubjectAsync(subjectId);
Assert.Equal(3, count);
}
[Fact]
public async Task CountBySubjectAsync_NoEvidence_ReturnsZero()
{
var count = await _store.CountBySubjectAsync("sha256:nonexistent");
Assert.Equal(0, count);
}
#endregion
#region Clear
[Fact]
public async Task Clear_RemovesAllEvidence()
{
await _store.StoreAsync(CreateTestEvidence("sha256:sub1"));
await _store.StoreAsync(CreateTestEvidence("sha256:sub2"));
_store.Clear();
Assert.Equal(0, _store.Count);
}
#endregion
#region Cancellation
[Fact]
public async Task StoreAsync_CancelledToken_ThrowsOperationCancelledException()
{
var cts = new CancellationTokenSource();
cts.Cancel();
var evidence = CreateTestEvidence("sha256:subject1");
await Assert.ThrowsAsync<OperationCanceledException>(() =>
_store.StoreAsync(evidence, cts.Token));
}
[Fact]
public async Task GetByIdAsync_CancelledToken_ThrowsOperationCancelledException()
{
var cts = new CancellationTokenSource();
cts.Cancel();
await Assert.ThrowsAsync<OperationCanceledException>(() =>
_store.GetByIdAsync("sha256:test", cts.Token));
}
#endregion
}

View File

@@ -0,0 +1,269 @@
// <copyright file="ProofSegmentAdapterTests.cs" company="StellaOps">
// SPDX-License-Identifier: AGPL-3.0-or-later
// </copyright>
using System.Collections.Immutable;
using StellaOps.Evidence.Core;
using StellaOps.Evidence.Core.Adapters;
namespace StellaOps.Evidence.Core.Tests;
public sealed class ProofSegmentAdapterTests
{
private readonly ProofSegmentAdapter _adapter = new();
private readonly string _subjectNodeId = "sha256:segment123";
private readonly EvidenceProvenance _provenance;
public ProofSegmentAdapterTests()
{
_provenance = new EvidenceProvenance
{
GeneratorId = "proof-spine",
GeneratorVersion = "1.0.0",
GeneratedAt = DateTimeOffset.Parse("2025-01-15T14:00:00Z")
};
}
[Fact]
public void CanConvert_WithValidSegment_ReturnsTrue()
{
var segment = CreateValidSegment();
var result = _adapter.CanConvert(segment);
Assert.True(result);
}
[Fact]
public void CanConvert_WithNullSegment_ReturnsFalse()
{
var result = _adapter.CanConvert(null!);
Assert.False(result);
}
[Fact]
public void CanConvert_WithEmptySegmentId_ReturnsFalse()
{
var segment = CreateValidSegment() with { SegmentId = "" };
var result = _adapter.CanConvert(segment);
Assert.False(result);
}
[Fact]
public void CanConvert_WithEmptyInputHash_ReturnsFalse()
{
var segment = CreateValidSegment() with { InputHash = "" };
var result = _adapter.CanConvert(segment);
Assert.False(result);
}
[Fact]
public void Convert_CreatesSingleRecord()
{
var segment = CreateValidSegment();
var results = _adapter.Convert(segment, _subjectNodeId, _provenance);
Assert.Single(results);
}
[Fact]
public void Convert_RecordHasCorrectSubjectNodeId()
{
var segment = CreateValidSegment();
var results = _adapter.Convert(segment, _subjectNodeId, _provenance);
Assert.Equal(_subjectNodeId, results[0].SubjectNodeId);
}
[Theory]
[InlineData("SbomSlice", EvidenceType.Artifact)]
[InlineData("Match", EvidenceType.Scan)]
[InlineData("Reachability", EvidenceType.Reachability)]
[InlineData("GuardAnalysis", EvidenceType.Guard)]
[InlineData("RuntimeObservation", EvidenceType.Runtime)]
[InlineData("PolicyEval", EvidenceType.Policy)]
public void Convert_MapsSegmentTypeToEvidenceType(string segmentType, EvidenceType expectedType)
{
var segment = CreateValidSegment() with { SegmentType = segmentType };
var results = _adapter.Convert(segment, _subjectNodeId, _provenance);
Assert.Equal(expectedType, results[0].EvidenceType);
}
[Fact]
public void Convert_UnknownSegmentType_DefaultsToCustomType()
{
var segment = CreateValidSegment() with { SegmentType = "UnknownType" };
var results = _adapter.Convert(segment, _subjectNodeId, _provenance);
Assert.Equal(EvidenceType.Custom, results[0].EvidenceType);
}
[Fact]
public void Convert_RecordHasNonEmptyPayload()
{
var segment = CreateValidSegment();
var results = _adapter.Convert(segment, _subjectNodeId, _provenance);
Assert.False(results[0].Payload.IsEmpty);
}
[Fact]
public void Convert_RecordHasPayloadSchemaVersion()
{
var segment = CreateValidSegment();
var results = _adapter.Convert(segment, _subjectNodeId, _provenance);
Assert.Equal("proof-segment/v1", results[0].PayloadSchemaVersion);
}
[Fact]
public void Convert_RecordHasEmptySignatures()
{
var segment = CreateValidSegment();
var results = _adapter.Convert(segment, _subjectNodeId, _provenance);
Assert.Empty(results[0].Signatures);
}
[Fact]
public void Convert_UsesProvidedProvenance()
{
var segment = CreateValidSegment();
var results = _adapter.Convert(segment, _subjectNodeId, _provenance);
Assert.Equal(_provenance.GeneratorId, results[0].Provenance.GeneratorId);
Assert.Equal(_provenance.GeneratorVersion, results[0].Provenance.GeneratorVersion);
}
[Fact]
public void Convert_RecordHasUniqueEvidenceId()
{
var segment = CreateValidSegment();
var results = _adapter.Convert(segment, _subjectNodeId, _provenance);
Assert.NotNull(results[0].EvidenceId);
Assert.NotEmpty(results[0].EvidenceId);
}
[Fact]
public void Convert_WithNullSubjectNodeId_ThrowsArgumentNullException()
{
var segment = CreateValidSegment();
Assert.Throws<ArgumentNullException>(() =>
_adapter.Convert(segment, null!, _provenance));
}
[Fact]
public void Convert_WithNullProvenance_ThrowsArgumentNullException()
{
var segment = CreateValidSegment();
Assert.Throws<ArgumentNullException>(() =>
_adapter.Convert(segment, _subjectNodeId, null!));
}
[Fact]
public void Convert_DifferentSegments_ProduceDifferentEvidenceIds()
{
var segment1 = CreateValidSegment() with { SegmentId = "seg-001" };
var segment2 = CreateValidSegment() with { SegmentId = "seg-002" };
var results1 = _adapter.Convert(segment1, _subjectNodeId, _provenance);
var results2 = _adapter.Convert(segment2, _subjectNodeId, _provenance);
Assert.NotEqual(results1[0].EvidenceId, results2[0].EvidenceId);
}
[Fact]
public void Convert_SameSegmentTwice_ProducesSameEvidenceId()
{
var segment = CreateValidSegment();
var results1 = _adapter.Convert(segment, _subjectNodeId, _provenance);
var results2 = _adapter.Convert(segment, _subjectNodeId, _provenance);
Assert.Equal(results1[0].EvidenceId, results2[0].EvidenceId);
}
[Theory]
[InlineData("Pending")]
[InlineData("Verified")]
[InlineData("Partial")]
[InlineData("Invalid")]
[InlineData("Untrusted")]
public void Convert_AllStatuses_Supported(string status)
{
var segment = CreateValidSegment() with { Status = status };
var results = _adapter.Convert(segment, _subjectNodeId, _provenance);
Assert.Single(results);
}
[Fact]
public void Convert_WithToolInfo_IncludesInPayload()
{
var segment = CreateValidSegment() with
{
ToolId = "trivy",
ToolVersion = "0.50.0"
};
var results = _adapter.Convert(segment, _subjectNodeId, _provenance);
Assert.False(results[0].Payload.IsEmpty);
}
[Fact]
public void Convert_WithPrevSegmentHash_IncludesInPayload()
{
var segment = CreateValidSegment() with { PrevSegmentHash = "sha256:prevhash" };
var results = _adapter.Convert(segment, _subjectNodeId, _provenance);
Assert.False(results[0].Payload.IsEmpty);
}
[Fact]
public void Convert_WithSpineId_IncludesInPayload()
{
var segment = CreateValidSegment() with { SpineId = "spine-001" };
var results = _adapter.Convert(segment, _subjectNodeId, _provenance);
Assert.False(results[0].Payload.IsEmpty);
}
private ProofSegmentInput CreateValidSegment()
{
return new ProofSegmentInput
{
SegmentId = "seg-default",
SegmentType = "Match",
Index = 0,
InputHash = "sha256:input123",
ResultHash = "sha256:result456",
PrevSegmentHash = null,
ToolId = "scanner",
ToolVersion = "1.0.0",
Status = "Verified",
SpineId = null
};
}
}

View File

@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<RootNamespace>StellaOps.Evidence.Core.Tests</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Evidence.Core\StellaOps.Evidence.Core.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,286 @@
// <copyright file="VexObservationAdapterTests.cs" company="StellaOps">
// SPDX-License-Identifier: AGPL-3.0-or-later
// </copyright>
using System.Collections.Immutable;
using StellaOps.Evidence.Core;
using StellaOps.Evidence.Core.Adapters;
namespace StellaOps.Evidence.Core.Tests;
public sealed class VexObservationAdapterTests
{
private readonly VexObservationAdapter _adapter = new();
private readonly string _subjectNodeId = "sha256:abc123";
private readonly EvidenceProvenance _provenance;
public VexObservationAdapterTests()
{
_provenance = new EvidenceProvenance
{
GeneratorId = "test-generator",
GeneratorVersion = "1.0.0",
GeneratedAt = DateTimeOffset.Parse("2025-01-15T10:00:00Z")
};
}
[Fact]
public void CanConvert_WithValidObservation_ReturnsTrue()
{
var observation = CreateValidObservation();
var result = _adapter.CanConvert(observation);
Assert.True(result);
}
[Fact]
public void CanConvert_WithNullObservation_ReturnsFalse()
{
var result = _adapter.CanConvert(null!);
Assert.False(result);
}
[Fact]
public void CanConvert_WithEmptyObservationId_ReturnsFalse()
{
var observation = CreateValidObservation() with { ObservationId = "" };
var result = _adapter.CanConvert(observation);
Assert.False(result);
}
[Fact]
public void CanConvert_WithEmptyProviderId_ReturnsFalse()
{
var observation = CreateValidObservation() with { ProviderId = "" };
var result = _adapter.CanConvert(observation);
Assert.False(result);
}
[Fact]
public void Convert_CreatesObservationLevelRecord()
{
var observation = CreateValidObservation();
var results = _adapter.Convert(observation, _subjectNodeId, _provenance);
Assert.NotEmpty(results);
var observationRecord = results[0];
Assert.Equal(EvidenceType.Provenance, observationRecord.EvidenceType);
Assert.Equal(_subjectNodeId, observationRecord.SubjectNodeId);
}
[Fact]
public void Convert_CreatesStatementRecordsForEachStatement()
{
var statements = ImmutableArray.Create(
CreateValidStatement("CVE-2024-1001", "product-a"),
CreateValidStatement("CVE-2024-1002", "product-b"),
CreateValidStatement("CVE-2024-1003", "product-c"));
var observation = CreateValidObservation() with { Statements = statements };
var results = _adapter.Convert(observation, _subjectNodeId, _provenance);
// 1 observation record + 3 statement records
Assert.Equal(4, results.Count);
// First is observation record
Assert.Equal(EvidenceType.Provenance, results[0].EvidenceType);
// Rest are VEX statement records
for (int i = 1; i < results.Count; i++)
{
Assert.Equal(EvidenceType.Vex, results[i].EvidenceType);
}
}
[Fact]
public void Convert_WithSingleStatement_CreatesCorrectRecords()
{
var observation = CreateValidObservation();
var results = _adapter.Convert(observation, _subjectNodeId, _provenance);
// 1 observation + 1 statement
Assert.Equal(2, results.Count);
}
[Fact]
public void Convert_WithEmptyStatements_CreatesOnlyObservationRecord()
{
var observation = CreateValidObservation() with { Statements = [] };
var results = _adapter.Convert(observation, _subjectNodeId, _provenance);
Assert.Single(results);
Assert.Equal(EvidenceType.Provenance, results[0].EvidenceType);
}
[Fact]
public void Convert_WithSignature_IncludesSignatureInRecords()
{
var signature = new VexObservationSignatureInput
{
Present = true,
Format = "ES256",
KeyId = "key-123",
Signature = "MEUCIQD+signature=="
};
var upstream = CreateValidUpstream() with { Signature = signature };
var observation = CreateValidObservation() with { Upstream = upstream };
var results = _adapter.Convert(observation, _subjectNodeId, _provenance);
// Both records should have signatures
foreach (var record in results)
{
Assert.NotEmpty(record.Signatures);
Assert.Equal("key-123", record.Signatures[0].SignerId);
Assert.Equal("ES256", record.Signatures[0].Algorithm);
}
}
[Fact]
public void Convert_WithoutSignature_CreatesRecordsWithEmptySignatures()
{
var signature = new VexObservationSignatureInput
{
Present = false,
Format = null,
KeyId = null,
Signature = null
};
var upstream = CreateValidUpstream() with { Signature = signature };
var observation = CreateValidObservation() with { Upstream = upstream };
var results = _adapter.Convert(observation, _subjectNodeId, _provenance);
foreach (var record in results)
{
Assert.Empty(record.Signatures);
}
}
[Fact]
public void Convert_UsesProvidedProvenance()
{
var observation = CreateValidObservation();
var results = _adapter.Convert(observation, _subjectNodeId, _provenance);
foreach (var record in results)
{
Assert.Equal(_provenance.GeneratorId, record.Provenance.GeneratorId);
Assert.Equal(_provenance.GeneratorVersion, record.Provenance.GeneratorVersion);
}
}
[Fact]
public void Convert_WithNullSubjectNodeId_ThrowsArgumentNullException()
{
var observation = CreateValidObservation();
Assert.Throws<ArgumentNullException>(() =>
_adapter.Convert(observation, null!, _provenance));
}
[Fact]
public void Convert_WithNullProvenance_ThrowsArgumentNullException()
{
var observation = CreateValidObservation();
Assert.Throws<ArgumentNullException>(() =>
_adapter.Convert(observation, _subjectNodeId, null!));
}
[Fact]
public void Convert_EachRecordHasUniqueEvidenceId()
{
var statements = ImmutableArray.Create(
CreateValidStatement("CVE-2024-1001", "product-a"),
CreateValidStatement("CVE-2024-1002", "product-b"));
var observation = CreateValidObservation() with { Statements = statements };
var results = _adapter.Convert(observation, _subjectNodeId, _provenance);
var evidenceIds = results.Select(r => r.EvidenceId).ToList();
Assert.Equal(evidenceIds.Count, evidenceIds.Distinct().Count());
}
[Fact]
public void Convert_RecordsHavePayloadSchemaVersion()
{
var observation = CreateValidObservation();
var results = _adapter.Convert(observation, _subjectNodeId, _provenance);
foreach (var record in results)
{
Assert.Equal("1.0.0", record.PayloadSchemaVersion);
}
}
private VexObservationInput CreateValidObservation()
{
return new VexObservationInput
{
ObservationId = "obs-001",
Tenant = "test-tenant",
ProviderId = "nvd",
StreamId = "cve-feed",
Upstream = CreateValidUpstream(),
Statements = [CreateValidStatement("CVE-2024-1000", "product-x")],
Content = new VexObservationContentInput
{
Format = "openvex",
SpecVersion = "0.2.0",
Raw = null
},
CreatedAt = DateTimeOffset.Parse("2025-01-15T08:00:00Z"),
Supersedes = [],
Attributes = ImmutableDictionary<string, string>.Empty
};
}
private VexObservationUpstreamInput CreateValidUpstream()
{
return new VexObservationUpstreamInput
{
UpstreamId = "upstream-001",
DocumentVersion = "1.0",
FetchedAt = DateTimeOffset.Parse("2025-01-15T07:00:00Z"),
ReceivedAt = DateTimeOffset.Parse("2025-01-15T07:30:00Z"),
ContentHash = "sha256:abc123",
Signature = new VexObservationSignatureInput
{
Present = false,
Format = null,
KeyId = null,
Signature = null
},
Metadata = ImmutableDictionary<string, string>.Empty
};
}
private VexObservationStatementInput CreateValidStatement(string vulnId, string productKey)
{
return new VexObservationStatementInput
{
VulnerabilityId = vulnId,
ProductKey = productKey,
Status = "not_affected",
LastObserved = DateTimeOffset.Parse("2025-01-15T06:00:00Z"),
Justification = "component_not_present",
Purl = "pkg:npm/example@1.0.0"
};
}
}