261 lines
8.4 KiB
C#
261 lines
8.4 KiB
C#
// <copyright file="EvidenceCardServiceTests.cs" company="StellaOps">
|
|
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
|
// Sprint: SPRINT_20260112_004_LB_evidence_card_core (EVPCARD-LB-004)
|
|
// Description: Tests for EvidenceCardService
|
|
// </copyright>
|
|
|
|
using System.Collections.Immutable;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using StellaOps.Determinism;
|
|
using StellaOps.Evidence.Pack;
|
|
using StellaOps.Evidence.Pack.Models;
|
|
using StellaOps.TestKit;
|
|
using Xunit;
|
|
|
|
namespace StellaOps.Evidence.Pack.Tests;
|
|
|
|
public sealed class EvidenceCardServiceTests
|
|
{
|
|
private readonly FixedGuidProvider _guidProvider = new(Guid.Parse("11111111-1111-1111-1111-111111111111"));
|
|
private readonly TestTimeProvider _timeProvider = new(new DateTimeOffset(2026, 1, 14, 10, 0, 0, TimeSpan.Zero));
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task CreateCardAsync_WithValidRequest_ReturnsCard()
|
|
{
|
|
var service = CreateService();
|
|
var request = new EvidenceCardRequest
|
|
{
|
|
FindingId = "CVE-2024-12345",
|
|
ArtifactDigest = "sha256:abc123",
|
|
ComponentPurl = "pkg:npm/lodash@4.17.21",
|
|
TenantId = "tenant-1"
|
|
};
|
|
|
|
var card = await service.CreateCardAsync(request);
|
|
|
|
Assert.NotNull(card);
|
|
Assert.Equal("11111111111111111111111111111111", card.CardId);
|
|
Assert.Equal("CVE-2024-12345", card.Subject.FindingId);
|
|
Assert.Equal("sha256:abc123", card.Subject.ArtifactDigest);
|
|
Assert.NotNull(card.Envelope);
|
|
Assert.NotNull(card.SbomExcerpt);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task CreateCardAsync_SetsGeneratedAtFromTimeProvider()
|
|
{
|
|
var service = CreateService();
|
|
var request = new EvidenceCardRequest
|
|
{
|
|
FindingId = "CVE-2024-12345",
|
|
ArtifactDigest = "sha256:abc123",
|
|
TenantId = "tenant-1"
|
|
};
|
|
|
|
var card = await service.CreateCardAsync(request);
|
|
|
|
Assert.Equal(_timeProvider.GetUtcNow(), card.GeneratedAt);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task CreateCardAsync_WithComponentPurl_ExtractsComponentInfo()
|
|
{
|
|
var service = CreateService();
|
|
var request = new EvidenceCardRequest
|
|
{
|
|
FindingId = "CVE-2024-12345",
|
|
ArtifactDigest = "sha256:abc123",
|
|
ComponentPurl = "pkg:npm/lodash@4.17.21",
|
|
TenantId = "tenant-1"
|
|
};
|
|
|
|
var card = await service.CreateCardAsync(request);
|
|
|
|
Assert.Single(card.SbomExcerpt.Components);
|
|
Assert.Equal("pkg:npm/lodash@4.17.21", card.SbomExcerpt.Components[0].Purl);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task ExportCardAsync_Json_ReturnsValidJson()
|
|
{
|
|
var service = CreateService();
|
|
var card = await CreateTestCard(service);
|
|
|
|
var export = await service.ExportCardAsync(card, EvidenceCardExportFormat.Json);
|
|
|
|
Assert.Equal("application/json", export.ContentType);
|
|
Assert.StartsWith("sha256:", export.ContentDigest);
|
|
|
|
var json = Encoding.UTF8.GetString(export.Content);
|
|
using var document = JsonDocument.Parse(json);
|
|
Assert.Equal(JsonValueKind.Object, document.RootElement.ValueKind);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task ExportCardAsync_CompactJson_IsSmallerThanIndented()
|
|
{
|
|
var service = CreateService();
|
|
var card = await CreateTestCard(service);
|
|
|
|
var jsonExport = await service.ExportCardAsync(card, EvidenceCardExportFormat.Json);
|
|
var compactExport = await service.ExportCardAsync(card, EvidenceCardExportFormat.CompactJson);
|
|
|
|
Assert.True(compactExport.Content.Length < jsonExport.Content.Length);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task ExportCardAsync_CanonicalJson_IsDeterministic()
|
|
{
|
|
var service1 = CreateService();
|
|
var service2 = CreateService();
|
|
|
|
var card1 = await CreateTestCard(service1);
|
|
var card2 = await CreateTestCard(service2);
|
|
|
|
var export1 = await service1.ExportCardAsync(card1, EvidenceCardExportFormat.CanonicalJson);
|
|
var export2 = await service2.ExportCardAsync(card2, EvidenceCardExportFormat.CanonicalJson);
|
|
|
|
Assert.Equal(export1.ContentDigest, export2.ContentDigest);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task VerifyCardAsync_ValidCard_ReturnsValid()
|
|
{
|
|
var service = CreateService();
|
|
var card = await CreateTestCard(service);
|
|
|
|
var result = await service.VerifyCardAsync(card);
|
|
|
|
Assert.True(result.Valid);
|
|
Assert.True(result.SignatureValid);
|
|
Assert.True(result.SbomDigestValid);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task VerifyCardAsync_WithMissingReceipt_AllowedByDefault()
|
|
{
|
|
var service = CreateService();
|
|
var card = await CreateTestCard(service);
|
|
|
|
var result = await service.VerifyCardAsync(card, new EvidenceCardVerificationOptions
|
|
{
|
|
AllowMissingReceipt = true
|
|
});
|
|
|
|
Assert.True(result.Valid);
|
|
Assert.Null(result.RekorReceiptValid);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task VerifyCardAsync_WithMissingReceipt_FailsWhenRequired()
|
|
{
|
|
var service = CreateService();
|
|
var card = await CreateTestCard(service);
|
|
|
|
var result = await service.VerifyCardAsync(card, new EvidenceCardVerificationOptions
|
|
{
|
|
AllowMissingReceipt = false
|
|
});
|
|
|
|
Assert.False(result.Valid);
|
|
Assert.Contains(result.Issues, i => i.Contains("Rekor receipt is required"));
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task VerifyCardAsync_WithValidRekorReceipt_ReturnsTrue()
|
|
{
|
|
var service = CreateService();
|
|
var card = await CreateTestCard(service);
|
|
|
|
// Add a valid-looking Rekor receipt
|
|
var cardWithReceipt = card with
|
|
{
|
|
RekorReceipt = new RekorReceiptMetadata
|
|
{
|
|
Uuid = "abc123def456",
|
|
LogIndex = 12345,
|
|
LogId = "0x1234",
|
|
LogUrl = "https://rekor.sigstore.dev",
|
|
IntegratedTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
|
|
RootHash = "sha256:root123",
|
|
TreeSize = 100000,
|
|
InclusionProofHashes = ImmutableArray.Create("hash1", "hash2"),
|
|
CheckpointNote = "rekor.sigstore.dev - 12345\n100000\nroot123\n",
|
|
CheckpointSignatures = ImmutableArray.Create(new CheckpointSignature
|
|
{
|
|
KeyId = "key1",
|
|
Signature = "c2lnbmF0dXJl"
|
|
})
|
|
}
|
|
};
|
|
|
|
var result = await service.VerifyCardAsync(cardWithReceipt);
|
|
|
|
Assert.True(result.Valid);
|
|
Assert.True(result.RekorReceiptValid);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task ExportCardAsync_SetsCorrectFileName()
|
|
{
|
|
var service = CreateService();
|
|
var card = await CreateTestCard(service);
|
|
|
|
var export = await service.ExportCardAsync(card, EvidenceCardExportFormat.Json);
|
|
|
|
Assert.Equal($"evidence-card-{card.CardId}.json", export.FileName);
|
|
}
|
|
|
|
private EvidenceCardService CreateService()
|
|
{
|
|
return new EvidenceCardService(
|
|
_timeProvider,
|
|
_guidProvider,
|
|
NullLogger<EvidenceCardService>.Instance);
|
|
}
|
|
|
|
private async Task<EvidenceCard> CreateTestCard(EvidenceCardService service)
|
|
{
|
|
var request = new EvidenceCardRequest
|
|
{
|
|
FindingId = "CVE-2024-12345",
|
|
ArtifactDigest = "sha256:abc123",
|
|
ComponentPurl = "pkg:npm/lodash@4.17.21",
|
|
TenantId = "tenant-1"
|
|
};
|
|
|
|
return await service.CreateCardAsync(request);
|
|
}
|
|
|
|
private sealed class FixedGuidProvider : IGuidProvider
|
|
{
|
|
private readonly Guid _guid;
|
|
|
|
public FixedGuidProvider(Guid guid) => _guid = guid;
|
|
|
|
public Guid NewGuid() => _guid;
|
|
}
|
|
|
|
private sealed class TestTimeProvider : TimeProvider
|
|
{
|
|
private readonly DateTimeOffset _fixedTime;
|
|
|
|
public TestTimeProvider(DateTimeOffset fixedTime) => _fixedTime = fixedTime;
|
|
|
|
public override DateTimeOffset GetUtcNow() => _fixedTime;
|
|
}
|
|
}
|