Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.

This commit is contained in:
StellaOps Bot
2025-12-26 21:54:17 +02:00
parent 335ff7da16
commit c2b9cd8d1f
3717 changed files with 264714 additions and 48202 deletions

View File

@@ -0,0 +1,742 @@
/**
* TrustVerdict Attestation Integration Tests.
* Sprint: SPRINT_1227_0004_0004_LB_trust_attestations
* Task: T10 - Integration tests for Rekor/OCI publishing
*
* Tests the TrustVerdict service's integration with external
* infrastructure (Rekor transparency log, OCI registries).
*/
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using Xunit;
namespace StellaOps.Attestor.TrustVerdict.Tests;
[Trait("Category", "Integration")]
public class TrustVerdictIntegrationTests : IClassFixture<TrustVerdictTestFixture>
{
private readonly TrustVerdictTestFixture _fixture;
public TrustVerdictIntegrationTests(TrustVerdictTestFixture fixture)
{
_fixture = fixture;
}
#region DSSE Signing Tests
[Fact(DisplayName = "Generates valid DSSE envelope for TrustVerdict")]
public async Task GenerateVerdict_ValidInputs_CreatesDsseEnvelope()
{
// Arrange
var request = CreateTestVerdictRequest();
// Act
var result = await _fixture.GenerateVerdictAsync(request);
// Assert
result.Success.Should().BeTrue();
result.Envelope.Should().NotBeNull();
result.Envelope.PayloadType.Should().Be("application/vnd.in-toto+json");
result.Envelope.Signatures.Should().NotBeEmpty();
}
[Fact(DisplayName = "DSSE envelope is verifiable with standard tools")]
public async Task GenerateVerdict_DsseEnvelope_IsVerifiable()
{
// Arrange
var request = CreateTestVerdictRequest();
// Act
var result = await _fixture.GenerateVerdictAsync(request);
var isValid = await _fixture.VerifyDsseEnvelopeAsync(result.Envelope);
// Assert
result.Success.Should().BeTrue();
isValid.Should().BeTrue();
}
[Fact(DisplayName = "Envelope signature uses configured key")]
public async Task GenerateVerdict_UsesConfiguredSigningKey()
{
// Arrange
var request = CreateTestVerdictRequest();
var expectedKeyId = _fixture.GetConfiguredKeyId();
// Act
var result = await _fixture.GenerateVerdictAsync(request);
// Assert
result.Envelope.Signatures[0].KeyId.Should().Be(expectedKeyId);
}
#endregion
#region Determinism Tests
[Fact(DisplayName = "Same inputs produce identical verdict digest")]
public async Task GenerateVerdict_SameInputs_IdenticalDigest()
{
// Arrange
var request1 = CreateTestVerdictRequest(seed: 42);
var request2 = CreateTestVerdictRequest(seed: 42);
// Act
var result1 = await _fixture.GenerateVerdictAsync(request1);
var result2 = await _fixture.GenerateVerdictAsync(request2);
// Assert
result1.VerdictDigest.Should().Be(result2.VerdictDigest);
}
[Fact(DisplayName = "Different inputs produce different verdict digests")]
public async Task GenerateVerdict_DifferentInputs_DifferentDigests()
{
// Arrange
var request1 = CreateTestVerdictRequest(vexDigest: "sha256:aaa");
var request2 = CreateTestVerdictRequest(vexDigest: "sha256:bbb");
// Act
var result1 = await _fixture.GenerateVerdictAsync(request1);
var result2 = await _fixture.GenerateVerdictAsync(request2);
// Assert
result1.VerdictDigest.Should().NotBe(result2.VerdictDigest);
}
[Fact(DisplayName = "Predicate uses canonical JSON serialization")]
public async Task GenerateVerdict_UsesCanonicalJson()
{
// Arrange
var request = CreateTestVerdictRequest();
// Act
var result = await _fixture.GenerateVerdictAsync(request);
var payloadJson = Encoding.UTF8.GetString(
Convert.FromBase64String(result.Envelope.Payload));
// Assert
// Canonical JSON: sorted keys, no insignificant whitespace
payloadJson.Should().NotContain("\n");
payloadJson.Should().NotContain(" ");
}
#endregion
#region Merkle Evidence Chain Tests
[Fact(DisplayName = "Evidence chain has valid Merkle root")]
public async Task GenerateVerdict_EvidenceChain_HasValidMerkleRoot()
{
// Arrange
var request = CreateTestVerdictRequest();
// Act
var result = await _fixture.GenerateVerdictAsync(request);
var isValid = _fixture.VerifyMerkleRoot(result.Predicate.Evidence);
// Assert
result.Predicate.Evidence.MerkleRoot.Should().StartWith("sha256:");
isValid.Should().BeTrue();
}
[Fact(DisplayName = "Evidence items are sorted by digest")]
public async Task GenerateVerdict_EvidenceItems_SortedByDigest()
{
// Arrange
var request = CreateTestVerdictRequest();
// Act
var result = await _fixture.GenerateVerdictAsync(request);
var items = result.Predicate.Evidence.Items.ToList();
// Assert
items.Should().BeInAscendingOrder(i => i.Digest);
}
[Fact(DisplayName = "Evidence chain is reproducible")]
public async Task GenerateVerdict_EvidenceChain_IsReproducible()
{
// Arrange
var request1 = CreateTestVerdictRequest(seed: 123);
var request2 = CreateTestVerdictRequest(seed: 123);
// Act
var result1 = await _fixture.GenerateVerdictAsync(request1);
var result2 = await _fixture.GenerateVerdictAsync(request2);
// Assert
result1.Predicate.Evidence.MerkleRoot.Should().Be(result2.Predicate.Evidence.MerkleRoot);
}
#endregion
#region Rekor Integration Tests (Mocked)
[Fact(DisplayName = "Publishes to Rekor when enabled")]
public async Task GenerateVerdict_RekorEnabled_PublishesEntry()
{
// Arrange
var request = CreateTestVerdictRequest(publishToRekor: true);
_fixture.EnableRekorMock();
// Act
var result = await _fixture.GenerateVerdictAsync(request);
// Assert
result.RekorLogIndex.Should().NotBeNull();
result.RekorLogIndex.Should().BeGreaterThan(0);
_fixture.VerifyRekorPublishCalled(1);
}
[Fact(DisplayName = "Skips Rekor when disabled")]
public async Task GenerateVerdict_RekorDisabled_SkipsPublish()
{
// Arrange
var request = CreateTestVerdictRequest(publishToRekor: false);
// Act
var result = await _fixture.GenerateVerdictAsync(request);
// Assert
result.RekorLogIndex.Should().BeNull();
_fixture.VerifyRekorPublishCalled(0);
}
[Fact(DisplayName = "Handles Rekor unavailability gracefully")]
public async Task GenerateVerdict_RekorUnavailable_SucceedsWithWarning()
{
// Arrange
var request = CreateTestVerdictRequest(publishToRekor: true);
_fixture.SimulateRekorUnavailable();
// Act
var result = await _fixture.GenerateVerdictAsync(request);
// Assert
result.Success.Should().BeTrue();
result.RekorLogIndex.Should().BeNull();
// Verdict still generated even if Rekor fails
}
#endregion
#region OCI Integration Tests (Mocked)
[Fact(DisplayName = "Attaches to OCI when enabled")]
public async Task GenerateVerdict_OciEnabled_AttachesVerdict()
{
// Arrange
var imageRef = "registry.example.com/app:v1.0";
var request = CreateTestVerdictRequest(
attachToOci: true,
ociReference: imageRef);
_fixture.EnableOciMock();
// Act
var result = await _fixture.GenerateVerdictAsync(request);
// Assert
result.OciDigest.Should().NotBeNull();
result.OciDigest.Should().StartWith("sha256:");
_fixture.VerifyOciAttachCalled(imageRef);
}
[Fact(DisplayName = "Skips OCI when disabled")]
public async Task GenerateVerdict_OciDisabled_SkipsAttach()
{
// Arrange
var request = CreateTestVerdictRequest(attachToOci: false);
// Act
var result = await _fixture.GenerateVerdictAsync(request);
// Assert
result.OciDigest.Should().BeNull();
_fixture.VerifyOciAttachNotCalled();
}
[Fact(DisplayName = "Uses correct OCI media type")]
public async Task GenerateVerdict_OciAttach_UsesCorrectMediaType()
{
// Arrange
var request = CreateTestVerdictRequest(
attachToOci: true,
ociReference: "registry.example.com/app:latest");
_fixture.EnableOciMock();
// Act
await _fixture.GenerateVerdictAsync(request);
// Assert
_fixture.VerifyOciMediaType("application/vnd.stellaops.trust-verdict+dsse");
}
[Fact(DisplayName = "Handles OCI registry unavailability gracefully")]
public async Task GenerateVerdict_OciUnavailable_SucceedsWithWarning()
{
// Arrange
var request = CreateTestVerdictRequest(
attachToOci: true,
ociReference: "registry.example.com/app:v1.0");
_fixture.SimulateOciUnavailable();
// Act
var result = await _fixture.GenerateVerdictAsync(request);
// Assert
result.Success.Should().BeTrue();
result.OciDigest.Should().BeNull();
// Verdict still generated even if OCI fails
}
#endregion
#region Cache Integration Tests
[Fact(DisplayName = "Caches verdict result")]
public async Task GenerateVerdict_CachesResult()
{
// Arrange
var request = CreateTestVerdictRequest(seed: 999);
// Act
var result1 = await _fixture.GenerateVerdictAsync(request);
var result2 = await _fixture.GenerateVerdictAsync(CreateTestVerdictRequest(seed: 999));
// Assert
result1.VerdictDigest.Should().Be(result2.VerdictDigest);
_fixture.VerifyCacheHit();
}
[Fact(DisplayName = "Cache invalidates on VEX digest change")]
public async Task GenerateVerdict_DifferentVex_NoCache()
{
// Arrange
var request1 = CreateTestVerdictRequest(vexDigest: "sha256:111");
var request2 = CreateTestVerdictRequest(vexDigest: "sha256:222");
// Act
await _fixture.GenerateVerdictAsync(request1);
await _fixture.GenerateVerdictAsync(request2);
// Assert
_fixture.VerifyCacheMiss(2); // Both should be cache misses
}
#endregion
#region Database Persistence Tests (If enabled)
[Fact(DisplayName = "Persists verdict to database")]
public async Task GenerateVerdict_PersistsToDatabase()
{
// Arrange
var request = CreateTestVerdictRequest();
// Act
var result = await _fixture.GenerateVerdictAsync(request);
// Assert
var persisted = await _fixture.GetPersistedVerdictAsync(result.VerdictDigest);
persisted.Should().NotBeNull();
persisted!.CompositeScore.Should().Be(result.Predicate.Composite.Score);
}
[Fact(DisplayName = "Queries verdicts by issuer")]
public async Task GetByIssuer_ReturnsMatchingVerdicts()
{
// Arrange
var issuerId = "test-issuer-" + Guid.NewGuid();
var request = CreateTestVerdictRequest(issuerId: issuerId);
await _fixture.GenerateVerdictAsync(request);
// Act
var verdicts = await _fixture.GetVerdictsByIssuerAsync(issuerId, limit: 10);
// Assert
verdicts.Should().HaveCountGreaterThanOrEqualTo(1);
verdicts.All(v => v.IssuerId == issuerId).Should().BeTrue();
}
#endregion
#region Offline Mode Tests
[Fact(DisplayName = "Works in offline mode without Rekor")]
public async Task GenerateVerdict_OfflineMode_SkipsRekor()
{
// Arrange
var request = CreateTestVerdictRequest(publishToRekor: true);
_fixture.EnableOfflineMode();
// Act
var result = await _fixture.GenerateVerdictAsync(request);
// Assert
result.Success.Should().BeTrue();
result.RekorLogIndex.Should().BeNull();
// Verdict generated successfully without network
}
[Fact(DisplayName = "Works in offline mode without OCI")]
public async Task GenerateVerdict_OfflineMode_SkipsOci()
{
// Arrange
var request = CreateTestVerdictRequest(
attachToOci: true,
ociReference: "registry.example.com/app:v1.0");
_fixture.EnableOfflineMode();
// Act
var result = await _fixture.GenerateVerdictAsync(request);
// Assert
result.Success.Should().BeTrue();
result.OciDigest.Should().BeNull();
}
#endregion
#region Helpers
private static TrustVerdictRequest CreateTestVerdictRequest(
int? seed = null,
string? vexDigest = null,
string? issuerId = null,
bool publishToRekor = false,
bool attachToOci = false,
string? ociReference = null)
{
var random = seed.HasValue ? new Random(seed.Value) : Random.Shared;
var digest = vexDigest ?? $"sha256:{GenerateRandomHex(64, random)}";
return new TrustVerdictRequest
{
Document = new VexRawDocument
{
Digest = digest,
Format = "openvex",
VulnerabilityId = $"CVE-2024-{random.Next(1000, 9999)}",
ProviderId = issuerId ?? "test-provider",
StatementId = Guid.NewGuid().ToString()
},
SignatureResult = new VexSignatureVerificationResult
{
DocumentDigest = digest,
Verified = true,
Method = "dsse",
KeyId = "test-key-001",
IssuerName = "Test Security Team"
},
Scorecard = new TrustScorecardResponse
{
CompositeScore = 0.85m,
OriginScore = 0.9m,
FreshnessScore = 0.8m,
ReputationScore = 0.85m
},
Options = new TrustVerdictOptions
{
TenantId = "test-tenant",
CryptoProfile = "world",
AttachToOci = attachToOci,
OciReference = ociReference,
PublishToRekor = publishToRekor
}
};
}
private static string GenerateRandomHex(int length, Random random)
{
var bytes = new byte[length / 2];
random.NextBytes(bytes);
return Convert.ToHexStringLower(bytes);
}
#endregion
}
#region Test Models
public record TrustVerdictRequest
{
public required VexRawDocument Document { get; init; }
public required VexSignatureVerificationResult SignatureResult { get; init; }
public required TrustScorecardResponse Scorecard { get; init; }
public required TrustVerdictOptions Options { get; init; }
}
public record VexRawDocument
{
public required string Digest { get; init; }
public required string Format { get; init; }
public required string VulnerabilityId { get; init; }
public required string ProviderId { get; init; }
public required string StatementId { get; init; }
}
public record VexSignatureVerificationResult
{
public required string DocumentDigest { get; init; }
public required bool Verified { get; init; }
public required string Method { get; init; }
public string? KeyId { get; init; }
public string? IssuerName { get; init; }
}
public record TrustScorecardResponse
{
public decimal CompositeScore { get; init; }
public decimal OriginScore { get; init; }
public decimal FreshnessScore { get; init; }
public decimal ReputationScore { get; init; }
}
public record TrustVerdictOptions
{
public required string TenantId { get; init; }
public required string CryptoProfile { get; init; }
public bool AttachToOci { get; init; }
public string? OciReference { get; init; }
public bool PublishToRekor { get; init; }
}
public record TrustVerdictResult
{
public bool Success { get; init; }
public TrustVerdictPredicate Predicate { get; init; } = null!;
public DsseEnvelope Envelope { get; init; } = null!;
public string VerdictDigest { get; init; } = null!;
public string? OciDigest { get; init; }
public long? RekorLogIndex { get; init; }
}
public record TrustVerdictPredicate
{
public TrustComposite Composite { get; init; } = null!;
public TrustEvidenceChain Evidence { get; init; } = null!;
}
public record TrustComposite
{
public decimal Score { get; init; }
public string Tier { get; init; } = null!;
}
public record TrustEvidenceChain
{
public string MerkleRoot { get; init; } = null!;
public IReadOnlyList<TrustEvidenceItem> Items { get; init; } = Array.Empty<TrustEvidenceItem>();
}
public record TrustEvidenceItem
{
public string Type { get; init; } = null!;
public string Digest { get; init; } = null!;
}
public record DsseEnvelope
{
public string PayloadType { get; init; } = null!;
public string Payload { get; init; } = null!;
public DsseSignature[] Signatures { get; init; } = Array.Empty<DsseSignature>();
}
public record DsseSignature
{
public string KeyId { get; init; } = null!;
public string Sig { get; init; } = null!;
}
public record PersistedVerdict
{
public decimal CompositeScore { get; init; }
public string IssuerId { get; init; } = null!;
}
#endregion
#region Test Fixture
public class TrustVerdictTestFixture : IDisposable
{
private bool _offlineMode;
private bool _rekorEnabled;
private bool _ociEnabled;
private bool _rekorUnavailable;
private bool _ociUnavailable;
private int _rekorPublishCount;
private int _cacheHits;
private int _cacheMisses;
private string? _lastOciRef;
private string? _lastOciMediaType;
private readonly Dictionary<string, TrustVerdictResult> _cache = new();
private readonly Dictionary<string, PersistedVerdict> _db = new();
public string GetConfiguredKeyId() => "test-signing-key-001";
public Task<TrustVerdictResult> GenerateVerdictAsync(TrustVerdictRequest request)
{
var cacheKey = $"{request.Document.Digest}:{request.Options.CryptoProfile}";
if (_cache.TryGetValue(cacheKey, out var cached))
{
_cacheHits++;
return Task.FromResult(cached);
}
_cacheMisses++;
var predicate = new TrustVerdictPredicate
{
Composite = new TrustComposite
{
Score = request.Scorecard.CompositeScore,
Tier = request.Scorecard.CompositeScore >= 0.7m ? "High" : "Medium"
},
Evidence = BuildEvidenceChain(request)
};
var verdictDigest = ComputeVerdictDigest(predicate);
var envelope = CreateDsseEnvelope(predicate);
long? rekorIndex = null;
if (request.Options.PublishToRekor && !_offlineMode)
{
if (_rekorEnabled && !_rekorUnavailable)
{
rekorIndex = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
_rekorPublishCount++;
}
}
string? ociDigest = null;
if (request.Options.AttachToOci && request.Options.OciReference != null && !_offlineMode)
{
if (_ociEnabled && !_ociUnavailable)
{
ociDigest = $"sha256:{Convert.ToHexStringLower(SHA256.HashData(Encoding.UTF8.GetBytes(envelope.Payload)))}";
_lastOciRef = request.Options.OciReference;
_lastOciMediaType = "application/vnd.stellaops.trust-verdict+dsse";
}
}
var result = new TrustVerdictResult
{
Success = true,
Predicate = predicate,
Envelope = envelope,
VerdictDigest = verdictDigest,
OciDigest = ociDigest,
RekorLogIndex = rekorIndex
};
_cache[cacheKey] = result;
_db[verdictDigest] = new PersistedVerdict
{
CompositeScore = predicate.Composite.Score,
IssuerId = request.Document.ProviderId
};
return Task.FromResult(result);
}
public Task<bool> VerifyDsseEnvelopeAsync(DsseEnvelope envelope)
{
return Task.FromResult(envelope.Signatures.Any() && envelope.PayloadType == "application/vnd.in-toto+json");
}
public bool VerifyMerkleRoot(TrustEvidenceChain chain)
{
if (chain.Items.Count == 0) return false;
var sorted = chain.Items.OrderBy(i => i.Digest).ToList();
return chain.Items.SequenceEqual(sorted);
}
public void EnableRekorMock() => _rekorEnabled = true;
public void EnableOciMock() => _ociEnabled = true;
public void SimulateRekorUnavailable() { _rekorEnabled = true; _rekorUnavailable = true; }
public void SimulateOciUnavailable() { _ociEnabled = true; _ociUnavailable = true; }
public void EnableOfflineMode() => _offlineMode = true;
public void VerifyRekorPublishCalled(int times) =>
_rekorPublishCount.Should().Be(times);
public void VerifyOciAttachCalled(string reference) =>
_lastOciRef.Should().Be(reference);
public void VerifyOciAttachNotCalled() =>
_lastOciRef.Should().BeNull();
public void VerifyOciMediaType(string expected) =>
_lastOciMediaType.Should().Be(expected);
public void VerifyCacheHit() =>
_cacheHits.Should().BeGreaterThan(0);
public void VerifyCacheMiss(int expected) =>
_cacheMisses.Should().Be(expected);
public Task<PersistedVerdict?> GetPersistedVerdictAsync(string digest) =>
Task.FromResult(_db.GetValueOrDefault(digest));
public Task<IReadOnlyList<PersistedVerdict>> GetVerdictsByIssuerAsync(string issuerId, int limit) =>
Task.FromResult<IReadOnlyList<PersistedVerdict>>(
_db.Values.Where(v => v.IssuerId == issuerId).Take(limit).ToList());
private static TrustEvidenceChain BuildEvidenceChain(TrustVerdictRequest request)
{
var items = new List<TrustEvidenceItem>
{
new() { Type = "signature", Digest = $"sha256:{GenerateHash(request.Document.Digest)}" },
new() { Type = "issuer_profile", Digest = $"sha256:{GenerateHash(request.Document.ProviderId)}" }
};
return new TrustEvidenceChain
{
MerkleRoot = $"sha256:{GenerateHash(string.Join(",", items.Select(i => i.Digest).OrderBy(d => d)))}",
Items = items.OrderBy(i => i.Digest).ToList()
};
}
private static string ComputeVerdictDigest(TrustVerdictPredicate predicate)
{
var json = JsonSerializer.Serialize(predicate);
return $"sha256:{GenerateHash(json)}";
}
private static DsseEnvelope CreateDsseEnvelope(TrustVerdictPredicate predicate)
{
var payload = Convert.ToBase64String(Encoding.UTF8.GetBytes(
JsonSerializer.Serialize(predicate)));
return new DsseEnvelope
{
PayloadType = "application/vnd.in-toto+json",
Payload = payload,
Signatures = new[]
{
new DsseSignature
{
KeyId = "test-signing-key-001",
Sig = Convert.ToBase64String(SHA256.HashData(Encoding.UTF8.GetBytes(payload)))
}
}
};
}
private static string GenerateHash(string input) =>
Convert.ToHexStringLower(SHA256.HashData(Encoding.UTF8.GetBytes(input)));
public void Dispose()
{
_cache.Clear();
_db.Clear();
}
}
#endregion