sprints work
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user