Files
git.stella-ops.org/src/Findings/StellaOps.Findings.Ledger.Tests/Attestation/AttestationPointerServiceTests.cs
StellaOps Bot 965cbf9574
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
Add unit tests for PhpFrameworkSurface and PhpPharScanner
- Implement comprehensive tests for PhpFrameworkSurface, covering scenarios such as empty surfaces, presence of routes, controllers, middlewares, CLI commands, cron jobs, and event listeners.
- Validate metadata creation for route counts, HTTP methods, protected and public routes, and route patterns.
- Introduce tests for PhpPharScanner, including handling of non-existent files, null or empty paths, invalid PHAR files, and minimal PHAR structures.
- Ensure correct computation of SHA256 for valid PHAR files and validate the properties of PhpPharArchive, PhpPharEntry, and PhpPharScanResult.
2025-12-07 13:44:13 +02:00

499 lines
20 KiB
C#

using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using StellaOps.Findings.Ledger.Domain;
using StellaOps.Findings.Ledger.Infrastructure;
using StellaOps.Findings.Ledger.Infrastructure.Attestation;
using StellaOps.Findings.Ledger.Services;
using Xunit;
namespace StellaOps.Findings.Ledger.Tests.Attestation;
public class AttestationPointerServiceTests
{
private readonly Mock<ILedgerEventRepository> _ledgerEventRepository;
private readonly Mock<ILedgerEventWriteService> _writeService;
private readonly InMemoryAttestationPointerRepository _repository;
private readonly FakeTimeProvider _timeProvider;
private readonly AttestationPointerService _service;
public AttestationPointerServiceTests()
{
_ledgerEventRepository = new Mock<ILedgerEventRepository>();
_writeService = new Mock<ILedgerEventWriteService>();
_repository = new InMemoryAttestationPointerRepository();
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 1, 1, 12, 0, 0, TimeSpan.Zero));
_writeService.Setup(w => w.AppendAsync(It.IsAny<LedgerEventDraft>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((LedgerEventDraft draft, CancellationToken _) =>
{
var record = new LedgerEventRecord(
draft.TenantId,
draft.ChainId,
draft.SequenceNumber,
draft.EventId,
draft.EventType,
draft.PolicyVersion,
draft.FindingId,
draft.ArtifactId,
draft.SourceRunId,
draft.ActorId,
draft.ActorType,
draft.OccurredAt,
draft.RecordedAt,
draft.Payload,
"event-hash",
draft.ProvidedPreviousHash ?? LedgerEventConstants.EmptyHash,
"merkle-leaf-hash",
draft.CanonicalEnvelope.ToJsonString());
return LedgerWriteResult.Success(record);
});
_service = new AttestationPointerService(
_ledgerEventRepository.Object,
_writeService.Object,
_repository,
_timeProvider,
NullLogger<AttestationPointerService>.Instance);
}
[Fact]
public async Task CreatePointer_CreatesNewPointer()
{
var input = new AttestationPointerInput(
TenantId: "tenant-1",
FindingId: "finding-123",
AttestationType: AttestationType.DsseEnvelope,
Relationship: AttestationRelationship.VerifiedBy,
AttestationRef: new AttestationRef(
Digest: "sha256:abc123def456789012345678901234567890123456789012345678901234abcd",
AttestationId: Guid.NewGuid(),
StorageUri: "s3://attestations/test.json",
PayloadType: "application/vnd.in-toto+json",
PredicateType: "https://slsa.dev/provenance/v1"),
VerificationResult: new VerificationResult(
Verified: true,
VerifiedAt: _timeProvider.GetUtcNow(),
Verifier: "stellaops-attestor",
VerifierVersion: "2025.01.0"),
CreatedBy: "test-system");
var result = await _service.CreatePointerAsync(input);
Assert.True(result.Success);
Assert.NotNull(result.PointerId);
Assert.NotNull(result.LedgerEventId);
Assert.Null(result.Error);
var saved = await _repository.GetByIdAsync("tenant-1", result.PointerId!.Value, CancellationToken.None);
Assert.NotNull(saved);
Assert.Equal(input.FindingId, saved!.FindingId);
Assert.Equal(input.AttestationType, saved.AttestationType);
Assert.Equal(input.AttestationRef.Digest, saved.AttestationRef.Digest);
}
[Fact]
public async Task CreatePointer_IsIdempotent()
{
var input = new AttestationPointerInput(
TenantId: "tenant-1",
FindingId: "finding-456",
AttestationType: AttestationType.SlsaProvenance,
Relationship: AttestationRelationship.AttestedBy,
AttestationRef: new AttestationRef(
Digest: "sha256:def456789012345678901234567890123456789012345678901234567890abcd"),
CreatedBy: "test-system");
var result1 = await _service.CreatePointerAsync(input);
var result2 = await _service.CreatePointerAsync(input);
Assert.True(result1.Success);
Assert.True(result2.Success);
Assert.Equal(result1.PointerId, result2.PointerId);
var pointers = await _repository.GetByFindingIdAsync("tenant-1", "finding-456", CancellationToken.None);
Assert.Single(pointers);
}
[Fact]
public async Task GetPointers_ReturnsAllPointersForFinding()
{
var input1 = new AttestationPointerInput(
TenantId: "tenant-1",
FindingId: "finding-multi",
AttestationType: AttestationType.DsseEnvelope,
Relationship: AttestationRelationship.VerifiedBy,
AttestationRef: new AttestationRef(
Digest: "sha256:aaa111222333444555666777888999000111222333444555666777888999000a"));
var input2 = new AttestationPointerInput(
TenantId: "tenant-1",
FindingId: "finding-multi",
AttestationType: AttestationType.VexAttestation,
Relationship: AttestationRelationship.DerivedFrom,
AttestationRef: new AttestationRef(
Digest: "sha256:bbb111222333444555666777888999000111222333444555666777888999000b"));
await _service.CreatePointerAsync(input1);
await _service.CreatePointerAsync(input2);
var pointers = await _service.GetPointersAsync("tenant-1", "finding-multi");
Assert.Equal(2, pointers.Count);
Assert.Contains(pointers, p => p.AttestationType == AttestationType.DsseEnvelope);
Assert.Contains(pointers, p => p.AttestationType == AttestationType.VexAttestation);
}
[Fact]
public async Task GetSummary_CalculatesCorrectCounts()
{
var verified = new AttestationPointerInput(
TenantId: "tenant-1",
FindingId: "finding-summary",
AttestationType: AttestationType.DsseEnvelope,
Relationship: AttestationRelationship.VerifiedBy,
AttestationRef: new AttestationRef(
Digest: "sha256:ver111222333444555666777888999000111222333444555666777888999000a"),
VerificationResult: new VerificationResult(Verified: true, VerifiedAt: _timeProvider.GetUtcNow()));
var unverified = new AttestationPointerInput(
TenantId: "tenant-1",
FindingId: "finding-summary",
AttestationType: AttestationType.SbomAttestation,
Relationship: AttestationRelationship.DerivedFrom,
AttestationRef: new AttestationRef(
Digest: "sha256:unv111222333444555666777888999000111222333444555666777888999000b"));
await _service.CreatePointerAsync(verified);
await _service.CreatePointerAsync(unverified);
var summary = await _service.GetSummaryAsync("tenant-1", "finding-summary");
Assert.Equal("finding-summary", summary.FindingId);
Assert.Equal(2, summary.AttestationCount);
Assert.Equal(1, summary.VerifiedCount);
Assert.Equal(OverallVerificationStatus.PartiallyVerified, summary.OverallVerificationStatus);
Assert.Contains(AttestationType.DsseEnvelope, summary.AttestationTypes);
Assert.Contains(AttestationType.SbomAttestation, summary.AttestationTypes);
}
[Fact]
public async Task Search_FiltersByAttestationType()
{
var input1 = new AttestationPointerInput(
TenantId: "tenant-1",
FindingId: "finding-search-1",
AttestationType: AttestationType.DsseEnvelope,
Relationship: AttestationRelationship.VerifiedBy,
AttestationRef: new AttestationRef(
Digest: "sha256:sea111222333444555666777888999000111222333444555666777888999000a"));
var input2 = new AttestationPointerInput(
TenantId: "tenant-1",
FindingId: "finding-search-2",
AttestationType: AttestationType.SlsaProvenance,
Relationship: AttestationRelationship.AttestedBy,
AttestationRef: new AttestationRef(
Digest: "sha256:sea222333444555666777888999000111222333444555666777888999000111b"));
await _service.CreatePointerAsync(input1);
await _service.CreatePointerAsync(input2);
var query = new AttestationPointerQuery(
TenantId: "tenant-1",
AttestationTypes: new[] { AttestationType.DsseEnvelope });
var results = await _service.SearchAsync(query);
Assert.Single(results);
Assert.Equal(AttestationType.DsseEnvelope, results[0].AttestationType);
}
[Fact]
public async Task UpdateVerificationResult_UpdatesExistingPointer()
{
var input = new AttestationPointerInput(
TenantId: "tenant-1",
FindingId: "finding-update",
AttestationType: AttestationType.DsseEnvelope,
Relationship: AttestationRelationship.VerifiedBy,
AttestationRef: new AttestationRef(
Digest: "sha256:upd111222333444555666777888999000111222333444555666777888999000a"));
var createResult = await _service.CreatePointerAsync(input);
Assert.True(createResult.Success);
var verificationResult = new VerificationResult(
Verified: true,
VerifiedAt: _timeProvider.GetUtcNow(),
Verifier: "external-verifier",
VerifierVersion: "1.0.0",
Checks: new[]
{
new VerificationCheck(VerificationCheckType.SignatureValid, true, "ECDSA verified"),
new VerificationCheck(VerificationCheckType.CertificateValid, true, "Chain verified")
});
var success = await _service.UpdateVerificationResultAsync(
"tenant-1", createResult.PointerId!.Value, verificationResult);
Assert.True(success);
var updated = await _repository.GetByIdAsync("tenant-1", createResult.PointerId!.Value, CancellationToken.None);
Assert.NotNull(updated?.VerificationResult);
Assert.True(updated.VerificationResult!.Verified);
Assert.Equal("external-verifier", updated.VerificationResult.Verifier);
Assert.Equal(2, updated.VerificationResult.Checks!.Count);
}
[Fact]
public async Task TenantIsolation_PreventsAccessAcrossTenants()
{
var input = new AttestationPointerInput(
TenantId: "tenant-a",
FindingId: "finding-isolated",
AttestationType: AttestationType.DsseEnvelope,
Relationship: AttestationRelationship.VerifiedBy,
AttestationRef: new AttestationRef(
Digest: "sha256:iso111222333444555666777888999000111222333444555666777888999000a"));
var result = await _service.CreatePointerAsync(input);
Assert.True(result.Success);
var fromTenantA = await _service.GetPointersAsync("tenant-a", "finding-isolated");
var fromTenantB = await _service.GetPointersAsync("tenant-b", "finding-isolated");
Assert.Single(fromTenantA);
Assert.Empty(fromTenantB);
}
private sealed class FakeTimeProvider : TimeProvider
{
private DateTimeOffset _now;
public FakeTimeProvider(DateTimeOffset now) => _now = now;
public override DateTimeOffset GetUtcNow() => _now;
public void Advance(TimeSpan duration) => _now = _now.Add(duration);
}
}
/// <summary>
/// In-memory implementation for testing.
/// </summary>
internal sealed class InMemoryAttestationPointerRepository : IAttestationPointerRepository
{
private readonly List<AttestationPointerRecord> _records = new();
private readonly object _lock = new();
public Task InsertAsync(AttestationPointerRecord record, CancellationToken cancellationToken)
{
lock (_lock)
{
_records.Add(record);
}
return Task.CompletedTask;
}
public Task<AttestationPointerRecord?> GetByIdAsync(string tenantId, Guid pointerId, CancellationToken cancellationToken)
{
lock (_lock)
{
var result = _records.FirstOrDefault(r => r.TenantId == tenantId && r.PointerId == pointerId);
return Task.FromResult(result);
}
}
public Task<IReadOnlyList<AttestationPointerRecord>> GetByFindingIdAsync(string tenantId, string findingId, CancellationToken cancellationToken)
{
lock (_lock)
{
var results = _records
.Where(r => r.TenantId == tenantId && r.FindingId == findingId)
.OrderByDescending(r => r.CreatedAt)
.ToList();
return Task.FromResult<IReadOnlyList<AttestationPointerRecord>>(results);
}
}
public Task<IReadOnlyList<AttestationPointerRecord>> GetByDigestAsync(string tenantId, string digest, CancellationToken cancellationToken)
{
lock (_lock)
{
var results = _records
.Where(r => r.TenantId == tenantId && r.AttestationRef.Digest == digest)
.OrderByDescending(r => r.CreatedAt)
.ToList();
return Task.FromResult<IReadOnlyList<AttestationPointerRecord>>(results);
}
}
public Task<IReadOnlyList<AttestationPointerRecord>> SearchAsync(AttestationPointerQuery query, CancellationToken cancellationToken)
{
lock (_lock)
{
var results = _records.Where(r => r.TenantId == query.TenantId);
if (query.FindingIds is { Count: > 0 })
{
results = results.Where(r => query.FindingIds.Contains(r.FindingId));
}
if (query.AttestationTypes is { Count: > 0 })
{
results = results.Where(r => query.AttestationTypes.Contains(r.AttestationType));
}
if (query.VerificationStatus.HasValue)
{
results = query.VerificationStatus.Value switch
{
AttestationVerificationFilter.Verified =>
results.Where(r => r.VerificationResult?.Verified == true),
AttestationVerificationFilter.Unverified =>
results.Where(r => r.VerificationResult is null),
AttestationVerificationFilter.Failed =>
results.Where(r => r.VerificationResult?.Verified == false),
_ => results
};
}
if (query.CreatedAfter.HasValue)
{
results = results.Where(r => r.CreatedAt >= query.CreatedAfter.Value);
}
if (query.CreatedBefore.HasValue)
{
results = results.Where(r => r.CreatedAt <= query.CreatedBefore.Value);
}
if (!string.IsNullOrWhiteSpace(query.SignerIdentity))
{
results = results.Where(r => r.AttestationRef.SignerInfo?.Subject == query.SignerIdentity);
}
if (!string.IsNullOrWhiteSpace(query.PredicateType))
{
results = results.Where(r => r.AttestationRef.PredicateType == query.PredicateType);
}
var list = results
.OrderByDescending(r => r.CreatedAt)
.Skip(query.Offset)
.Take(query.Limit)
.ToList();
return Task.FromResult<IReadOnlyList<AttestationPointerRecord>>(list);
}
}
public Task<FindingAttestationSummary> GetSummaryAsync(string tenantId, string findingId, CancellationToken cancellationToken)
{
lock (_lock)
{
var pointers = _records.Where(r => r.TenantId == tenantId && r.FindingId == findingId).ToList();
if (pointers.Count == 0)
{
return Task.FromResult(new FindingAttestationSummary(
findingId, 0, 0, null, Array.Empty<AttestationType>(), OverallVerificationStatus.NoAttestations));
}
var verifiedCount = pointers.Count(p => p.VerificationResult?.Verified == true);
var latest = pointers.Max(p => p.CreatedAt);
var types = pointers.Select(p => p.AttestationType).Distinct().ToList();
var status = pointers.Count switch
{
0 => OverallVerificationStatus.NoAttestations,
_ when verifiedCount == pointers.Count => OverallVerificationStatus.AllVerified,
_ when verifiedCount > 0 => OverallVerificationStatus.PartiallyVerified,
_ => OverallVerificationStatus.NoneVerified
};
return Task.FromResult(new FindingAttestationSummary(
findingId, pointers.Count, verifiedCount, latest, types, status));
}
}
public Task<IReadOnlyList<FindingAttestationSummary>> GetSummariesAsync(string tenantId, IReadOnlyList<string> findingIds, CancellationToken cancellationToken)
{
var tasks = findingIds.Select(fid => GetSummaryAsync(tenantId, fid, cancellationToken));
return Task.WhenAll(tasks).ContinueWith(t => (IReadOnlyList<FindingAttestationSummary>)t.Result.ToList());
}
public Task<bool> ExistsAsync(string tenantId, string findingId, string digest, AttestationType attestationType, CancellationToken cancellationToken)
{
lock (_lock)
{
var exists = _records.Any(r =>
r.TenantId == tenantId &&
r.FindingId == findingId &&
r.AttestationRef.Digest == digest &&
r.AttestationType == attestationType);
return Task.FromResult(exists);
}
}
public Task UpdateVerificationResultAsync(string tenantId, Guid pointerId, VerificationResult verificationResult, CancellationToken cancellationToken)
{
lock (_lock)
{
var idx = _records.FindIndex(r => r.TenantId == tenantId && r.PointerId == pointerId);
if (idx >= 0)
{
var old = _records[idx];
_records[idx] = old with { VerificationResult = verificationResult };
}
}
return Task.CompletedTask;
}
public Task<int> GetCountAsync(string tenantId, string findingId, CancellationToken cancellationToken)
{
lock (_lock)
{
var count = _records.Count(r => r.TenantId == tenantId && r.FindingId == findingId);
return Task.FromResult(count);
}
}
public Task<IReadOnlyList<string>> GetFindingIdsWithAttestationsAsync(string tenantId, AttestationVerificationFilter? verificationFilter, IReadOnlyList<AttestationType>? attestationTypes, int limit, int offset, CancellationToken cancellationToken)
{
lock (_lock)
{
var results = _records.Where(r => r.TenantId == tenantId);
if (attestationTypes is { Count: > 0 })
{
results = results.Where(r => attestationTypes.Contains(r.AttestationType));
}
if (verificationFilter.HasValue)
{
results = verificationFilter.Value switch
{
AttestationVerificationFilter.Verified =>
results.Where(r => r.VerificationResult?.Verified == true),
AttestationVerificationFilter.Unverified =>
results.Where(r => r.VerificationResult is null),
AttestationVerificationFilter.Failed =>
results.Where(r => r.VerificationResult?.Verified == false),
_ => results
};
}
var list = results
.Select(r => r.FindingId)
.Distinct()
.OrderBy(f => f)
.Skip(offset)
.Take(limit)
.ToList();
return Task.FromResult<IReadOnlyList<string>>(list);
}
}
}