This commit is contained in:
StellaOps Bot
2025-12-09 00:20:52 +02:00
parent 3d01bf9edc
commit bc0762e97d
261 changed files with 14033 additions and 4427 deletions

View File

@@ -0,0 +1,18 @@
using FluentAssertions;
using StellaOps.Findings.Ledger.Infrastructure.Attestation;
namespace StellaOps.Findings.Ledger.Tests;
public class AttestationStatusCalculatorTests
{
[Theory]
[InlineData(0, 0, OverallVerificationStatus.NoAttestations)]
[InlineData(3, 3, OverallVerificationStatus.AllVerified)]
[InlineData(4, 1, OverallVerificationStatus.PartiallyVerified)]
[InlineData(2, 0, OverallVerificationStatus.NoneVerified)]
public void Compute_ReturnsExpectedStatus(int attestationCount, int verifiedCount, OverallVerificationStatus expected)
{
AttestationStatusCalculator.Compute(attestationCount, verifiedCount)
.Should().Be(expected);
}
}

View File

@@ -0,0 +1,20 @@
using FluentAssertions;
using Microsoft.AspNetCore.Http;
using StellaOps.Findings.Ledger;
namespace StellaOps.Findings.Ledger.Tests;
public class DeprecationHeadersTests
{
[Fact]
public void Apply_SetsStandardDeprecationHeaders()
{
var context = new DefaultHttpContext();
DeprecationHeaders.Apply(context.Response, "ledger.export.findings");
context.Response.Headers["Deprecation"].ToString().Should().Be("true");
context.Response.Headers["Sunset"].ToString().Should().Be(DeprecationHeaders.SunsetDate);
context.Response.Headers["X-Deprecated-Endpoint"].ToString().Should().Be("ledger.export.findings");
context.Response.Headers["Link"].ToString().Should().Contain("/.well-known/openapi");
}
}

View File

@@ -0,0 +1,28 @@
using System.Text;
using FluentAssertions;
using StellaOps.Findings.Ledger.OpenApi;
namespace StellaOps.Findings.Ledger.Tests;
public class OpenApiMetadataFactoryTests
{
[Fact]
public void ComputeEtag_IsDeterministicAndWeak()
{
var bytes = Encoding.UTF8.GetBytes("spec-content");
var etag1 = OpenApiMetadataFactory.ComputeEtag(bytes);
var etag2 = OpenApiMetadataFactory.ComputeEtag(bytes);
etag1.Should().StartWith("W/\"");
etag1.Should().Be(etag2);
etag1.Length.Should().BeGreaterThan(6);
}
[Fact]
public void GetSpecPath_ResolvesExistingSpec()
{
var path = OpenApiMetadataFactory.GetSpecPath(AppContext.BaseDirectory);
File.Exists(path).Should().BeTrue();
}
}

View File

@@ -0,0 +1,39 @@
using System.Text;
using FluentAssertions;
using StellaOps.Findings.Ledger.OpenApi;
namespace StellaOps.Findings.Ledger.Tests;
public class OpenApiSdkSurfaceTests
{
private readonly string _specContent;
public OpenApiSdkSurfaceTests()
{
var path = OpenApiMetadataFactory.GetSpecPath(AppContext.BaseDirectory);
_specContent = File.ReadAllText(path, Encoding.UTF8);
}
[Fact]
public void FindingsEndpoints_ExposePaginationAndFilters()
{
_specContent.Should().Contain("/findings");
_specContent.Should().Contain("page_token");
_specContent.Should().MatchRegex("nextPageToken|next_page_token");
}
[Fact]
public void EvidenceSchemas_ExposeEvidenceLinks()
{
_specContent.Should().Contain("evidenceBundleRef");
_specContent.Should().Contain("ExportProvenance");
}
[Fact]
public void AttestationPointers_ExposeProvenanceMetadata()
{
_specContent.Should().Contain("/v1/ledger/attestations");
_specContent.Should().Contain("attestation");
_specContent.Should().Contain("provenance");
}
}

View File

@@ -0,0 +1,116 @@
using System.Text.Json.Nodes;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Findings.Ledger.Domain;
using StellaOps.Findings.Ledger.Infrastructure;
using StellaOps.Findings.Ledger.Infrastructure.Attestation;
using StellaOps.Findings.Ledger.Services;
using FluentAssertions;
namespace StellaOps.Findings.Ledger.Tests;
public class ScoredFindingsQueryServiceTests
{
[Fact]
public async Task QueryAsync_MapsAttestationMetadata()
{
var projection = new FindingProjection(
TenantId: "tenant-a",
FindingId: "finding-123",
PolicyVersion: "v1",
Status: "affected",
Severity: 7.5m,
RiskScore: 0.9m,
RiskSeverity: "critical",
RiskProfileVersion: "p1",
RiskExplanationId: Guid.NewGuid(),
RiskEventSequence: 42,
Labels: new(),
CurrentEventId: Guid.NewGuid(),
ExplainRef: "explain-1",
PolicyRationale: new(),
UpdatedAt: DateTimeOffset.UtcNow,
CycleHash: "abc123",
AttestationCount: 3,
VerifiedAttestationCount: 2,
FailedAttestationCount: 1,
UnverifiedAttestationCount: 0,
AttestationStatus: OverallVerificationStatus.PartiallyVerified);
var repo = new FakeFindingProjectionRepository(projection);
var service = new ScoredFindingsQueryService(
repo,
new NullRiskExplanationStore(),
TimeProvider.System,
NullLogger<ScoredFindingsQueryService>.Instance);
var result = await service.QueryAsync(new ScoredFindingsQuery
{
TenantId = "tenant-a",
Limit = 10
});
result.TotalCount.Should().Be(1);
result.Findings.Should().HaveCount(1);
var finding = result.Findings.Single();
finding.AttestationCount.Should().Be(3);
finding.VerifiedAttestationCount.Should().Be(2);
finding.FailedAttestationCount.Should().Be(1);
finding.UnverifiedAttestationCount.Should().Be(0);
finding.AttestationStatus.Should().Be(OverallVerificationStatus.PartiallyVerified);
}
private sealed class FakeFindingProjectionRepository : IFindingProjectionRepository
{
private readonly FindingProjection _projection;
public FakeFindingProjectionRepository(FindingProjection projection)
{
_projection = projection;
}
public Task<ProjectionCheckpoint> GetCheckpointAsync(CancellationToken cancellationToken) =>
Task.FromResult(ProjectionCheckpoint.Initial(TimeProvider.System));
public Task<(IReadOnlyList<FindingProjection> Projections, int TotalCount)> QueryScoredAsync(
ScoredFindingsQuery query,
CancellationToken cancellationToken) =>
Task.FromResult((new List<FindingProjection> { _projection } as IReadOnlyList<FindingProjection>, 1));
public Task<FindingStatsResult> GetFindingStatsSinceAsync(string tenantId, DateTimeOffset since, CancellationToken cancellationToken) =>
Task.FromResult(new FindingStatsResult(0, 0, 0, 0, 0, 0));
public Task<(int Total, int Scored, decimal AvgScore, decimal MaxScore)> GetRiskAggregatesAsync(string tenantId, string? policyVersion, CancellationToken cancellationToken) =>
Task.FromResult((0, 0, 0m, 0m));
public Task<ScoreDistribution> GetScoreDistributionAsync(string tenantId, string? policyVersion, CancellationToken cancellationToken) =>
Task.FromResult(new ScoreDistribution());
public Task<SeverityDistribution> GetSeverityDistributionAsync(string tenantId, string? policyVersion, CancellationToken cancellationToken) =>
Task.FromResult(new SeverityDistribution());
public Task SaveCheckpointAsync(ProjectionCheckpoint checkpoint, CancellationToken cancellationToken) =>
Task.CompletedTask;
public Task InsertHistoryAsync(FindingHistoryEntry entry, CancellationToken cancellationToken) =>
Task.CompletedTask;
public Task InsertActionAsync(TriageActionEntry entry, CancellationToken cancellationToken) =>
Task.CompletedTask;
public Task<FindingProjection?> GetAsync(string tenantId, string findingId, string policyVersion, CancellationToken cancellationToken) =>
Task.FromResult<FindingProjection?>(_projection);
public Task UpsertAsync(FindingProjection projection, CancellationToken cancellationToken) =>
Task.CompletedTask;
}
private sealed class NullRiskExplanationStore : IRiskExplanationStore
{
public Task<ScoredFindingExplanation?> GetAsync(string tenantId, string findingId, Guid? explanationId, CancellationToken cancellationToken) =>
Task.FromResult<ScoredFindingExplanation?>(null);
public Task StoreAsync(string tenantId, ScoredFindingExplanation explanation, CancellationToken cancellationToken) =>
Task.CompletedTask;
}
}