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,230 @@
// -----------------------------------------------------------------------------
// AuditPackExportServiceTests.cs
// Sprint: SPRINT_1227_0005_0003_FE_copy_audit_export
// Task: T10 — Unit tests for AuditPackExportService
// -----------------------------------------------------------------------------
namespace StellaOps.AuditPack.Tests;
using StellaOps.AuditPack.Models;
using StellaOps.AuditPack.Services;
using System.IO.Compression;
using System.Text.Json;
[Trait("Category", "Unit")]
public class AuditPackExportServiceTests
{
private readonly AuditPackExportService _service;
public AuditPackExportServiceTests()
{
_service = new AuditPackExportService(
new MockAuditBundleWriter(),
null);
}
[Fact]
public async Task ExportAsync_Zip_CreatesValidZipArchive()
{
// Arrange
var request = new ExportRequest
{
ScanId = "scan-123",
Format = ExportFormat.Zip,
Segments = [ExportSegment.Sbom, ExportSegment.Match],
IncludeAttestations = true,
IncludeProofChain = false,
Filename = "test-export"
};
// Act
var result = await _service.ExportAsync(request);
// Assert
result.Success.Should().BeTrue();
result.ContentType.Should().Be("application/zip");
result.Filename.Should().Be("test-export.zip");
result.Data.Should().NotBeNull();
result.SizeBytes.Should().BeGreaterThan(0);
// Verify ZIP structure
using var memoryStream = new MemoryStream(result.Data!);
using var archive = new ZipArchive(memoryStream, ZipArchiveMode.Read);
archive.Entries.Should().Contain(e => e.FullName == "manifest.json");
}
[Fact]
public async Task ExportAsync_Json_CreatesSingleJsonFile()
{
// Arrange
var request = new ExportRequest
{
ScanId = "scan-456",
Format = ExportFormat.Json,
Segments = [ExportSegment.Sbom],
IncludeAttestations = false,
IncludeProofChain = false,
Filename = "test-json"
};
// Act
var result = await _service.ExportAsync(request);
// Assert
result.Success.Should().BeTrue();
result.ContentType.Should().Be("application/json");
result.Filename.Should().Be("test-json.json");
// Verify JSON structure
using var jsonDoc = JsonDocument.Parse(result.Data!);
var root = jsonDoc.RootElement;
root.TryGetProperty("scanId", out var scanIdProp).Should().BeTrue();
scanIdProp.GetString().Should().Be("scan-456");
root.TryGetProperty("segments", out _).Should().BeTrue();
}
[Fact]
public async Task ExportAsync_Dsse_CreatesDsseEnvelope()
{
// Arrange
var request = new ExportRequest
{
ScanId = "scan-789",
Format = ExportFormat.Dsse,
Segments = [ExportSegment.Policy],
IncludeAttestations = true,
IncludeProofChain = true,
Filename = "test-dsse"
};
// Act
var result = await _service.ExportAsync(request);
// Assert
result.Success.Should().BeTrue();
result.ContentType.Should().Be("application/vnd.dsse+json");
result.Filename.Should().Be("test-dsse.dsse.json");
// Verify DSSE structure
using var jsonDoc = JsonDocument.Parse(result.Data!);
var root = jsonDoc.RootElement;
root.TryGetProperty("payloadType", out var payloadType).Should().BeTrue();
payloadType.GetString().Should().Be("application/vnd.stellaops.audit-pack+json");
root.TryGetProperty("payload", out _).Should().BeTrue();
root.TryGetProperty("signatures", out _).Should().BeTrue();
}
[Fact]
public async Task ExportAsync_AllSegments_IncludesAllInZip()
{
// Arrange
var allSegments = new[]
{
ExportSegment.Sbom,
ExportSegment.Match,
ExportSegment.Reachability,
ExportSegment.Guards,
ExportSegment.Runtime,
ExportSegment.Policy
};
var request = new ExportRequest
{
ScanId = "scan-full",
Format = ExportFormat.Zip,
Segments = allSegments,
IncludeAttestations = true,
IncludeProofChain = true,
Filename = "full-export"
};
// Act
var result = await _service.ExportAsync(request);
// Assert
result.Success.Should().BeTrue();
using var memoryStream = new MemoryStream(result.Data!);
using var archive = new ZipArchive(memoryStream, ZipArchiveMode.Read);
// Should have manifest + 6 segments + attestations + proof chain
archive.Entries.Count.Should().BeGreaterThanOrEqualTo(3);
}
[Fact]
public async Task ExportAsync_EmptySegments_StillCreatesValidExport()
{
// Arrange
var request = new ExportRequest
{
ScanId = "scan-empty",
Format = ExportFormat.Json,
Segments = [],
IncludeAttestations = false,
IncludeProofChain = false,
Filename = "empty-export"
};
// Act
var result = await _service.ExportAsync(request);
// Assert
result.Success.Should().BeTrue();
result.Data.Should().NotBeNull();
}
[Fact]
public async Task ExportAsync_UnsupportedFormat_ReturnsFailed()
{
// Arrange
var request = new ExportRequest
{
ScanId = "scan-fail",
Format = (ExportFormat)999, // Invalid format
Segments = [ExportSegment.Sbom],
IncludeAttestations = false,
IncludeProofChain = false,
Filename = "fail-export"
};
// Act
var result = await _service.ExportAsync(request);
// Assert
result.Success.Should().BeFalse();
result.Error.Should().Contain("Unsupported");
}
[Fact]
public async Task ExportAsync_CancellationRequested_ThrowsOperationCanceled()
{
// Arrange
var request = new ExportRequest
{
ScanId = "scan-cancel",
Format = ExportFormat.Zip,
Segments = [ExportSegment.Sbom],
IncludeAttestations = false,
IncludeProofChain = false,
Filename = "cancel-export"
};
var cts = new CancellationTokenSource();
cts.Cancel();
// Act & Assert
await Assert.ThrowsAsync<OperationCanceledException>(
() => _service.ExportAsync(request, cts.Token));
}
// Mock implementation for testing
private class MockAuditBundleWriter : IAuditBundleWriter
{
public Task<AuditBundleWriteResult> WriteAsync(
AuditBundleWriteRequest request,
CancellationToken cancellationToken = default)
{
return Task.FromResult(new AuditBundleWriteResult { Success = true });
}
}
}

View File

@@ -0,0 +1,258 @@
// -----------------------------------------------------------------------------
// ReplayAttestationServiceTests.cs
// Sprint: SPRINT_1227_0005_0004_BE_verdict_replay
// Task: T8 — Unit tests for ReplayAttestationService
// -----------------------------------------------------------------------------
namespace StellaOps.AuditPack.Tests;
using StellaOps.AuditPack.Models;
using StellaOps.AuditPack.Services;
using System.Collections.Immutable;
[Trait("Category", "Unit")]
public class ReplayAttestationServiceTests
{
private readonly ReplayAttestationService _service;
public ReplayAttestationServiceTests()
{
_service = new ReplayAttestationService(null);
}
private static AuditBundleManifest CreateTestManifest(string bundleId = "bundle-123")
{
return new AuditBundleManifest
{
BundleId = bundleId,
Name = "Test Bundle",
CreatedAt = DateTimeOffset.UtcNow,
ScanId = "scan-123",
ImageRef = "docker.io/library/alpine:3.18",
ImageDigest = "sha256:abc123",
MerkleRoot = "sha256:merkle-root",
VerdictDigest = "sha256:verdict-digest",
Decision = "pass",
Inputs = new InputDigests
{
SbomDigest = "sha256:sbom-digest",
FeedsDigest = "sha256:feeds-digest",
PolicyDigest = "sha256:policy-digest"
},
Files = ImmutableArray<BundleFileEntry>.Empty
};
}
private static ReplayExecutionResult CreateTestResult(bool match = true)
{
return new ReplayExecutionResult
{
Success = true,
Status = match ? ReplayStatus.Match : ReplayStatus.Drift,
VerdictMatches = match,
DecisionMatches = match,
OriginalVerdictDigest = "sha256:verdict-digest",
ReplayedVerdictDigest = match ? "sha256:verdict-digest" : "sha256:different-digest",
OriginalDecision = "pass",
ReplayedDecision = match ? "pass" : "warn",
Drifts = match ? [] : [new DriftItem { Type = DriftType.Decision, Field = "decision" }],
DurationMs = 150,
EvaluatedAt = DateTimeOffset.UtcNow
};
}
[Fact]
public async Task GenerateAsync_CreatesValidAttestation()
{
// Arrange
var manifest = CreateTestManifest();
var result = CreateTestResult(match: true);
// Act
var attestation = await _service.GenerateAsync(manifest, result);
// Assert
attestation.Should().NotBeNull();
attestation.AttestationId.Should().NotBeNullOrEmpty();
attestation.ManifestId.Should().Be("bundle-123");
attestation.Match.Should().BeTrue();
attestation.ReplayStatus.Should().Be("Match");
attestation.Statement.Should().NotBeNull();
attestation.StatementDigest.Should().StartWith("sha256:");
}
[Fact]
public async Task GenerateAsync_IncludesInTotoStatement()
{
// Arrange
var manifest = CreateTestManifest();
var result = CreateTestResult();
// Act
var attestation = await _service.GenerateAsync(manifest, result);
// Assert
attestation.Statement.Type.Should().Be("https://in-toto.io/Statement/v1");
attestation.Statement.PredicateType.Should().Be("https://stellaops.io/attestation/verdict-replay/v1");
attestation.Statement.Subject.Should().HaveCount(1);
attestation.Statement.Subject[0].Name.Should().StartWith("verdict:");
}
[Fact]
public async Task GenerateAsync_IncludesPredicate()
{
// Arrange
var manifest = CreateTestManifest();
var result = CreateTestResult();
// Act
var attestation = await _service.GenerateAsync(manifest, result);
var predicate = attestation.Statement.Predicate;
// Assert
predicate.ManifestId.Should().Be("bundle-123");
predicate.ScanId.Should().Be("scan-123");
predicate.ImageRef.Should().Be("docker.io/library/alpine:3.18");
predicate.Match.Should().BeTrue();
predicate.Status.Should().Be("Match");
predicate.DurationMs.Should().Be(150);
}
[Fact]
public async Task GenerateAsync_CreatesDsseEnvelope()
{
// Arrange
var manifest = CreateTestManifest();
var result = CreateTestResult();
// Act
var attestation = await _service.GenerateAsync(manifest, result);
// Assert
attestation.Envelope.Should().NotBeNull();
attestation.Envelope!.PayloadType.Should().Be("application/vnd.in-toto+json");
attestation.Envelope.Payload.Should().NotBeNullOrEmpty();
// Without signer, signatures should be empty
attestation.Envelope.Signatures.Should().BeEmpty();
}
[Fact]
public async Task GenerateAsync_DriftResult_RecordsDivergence()
{
// Arrange
var manifest = CreateTestManifest();
var result = CreateTestResult(match: false);
// Act
var attestation = await _service.GenerateAsync(manifest, result);
var predicate = attestation.Statement.Predicate;
// Assert
attestation.Match.Should().BeFalse();
attestation.ReplayStatus.Should().Be("Drift");
predicate.Match.Should().BeFalse();
predicate.DriftCount.Should().Be(1);
predicate.Drifts.Should().HaveCount(1);
}
[Fact]
public async Task VerifyAsync_ValidAttestation_ReturnsValid()
{
// Arrange
var manifest = CreateTestManifest();
var result = CreateTestResult();
var attestation = await _service.GenerateAsync(manifest, result);
// Act
var verificationResult = await _service.VerifyAsync(attestation);
// Assert
verificationResult.IsValid.Should().BeTrue();
verificationResult.Errors.Should().BeEmpty();
}
[Fact]
public async Task GenerateBatchAsync_MultipleReplays_CreatesMultipleAttestations()
{
// Arrange
var replays = new[]
{
(CreateTestManifest("bundle-1"), CreateTestResult()),
(CreateTestManifest("bundle-2"), CreateTestResult(match: false)),
(CreateTestManifest("bundle-3"), CreateTestResult())
};
// Act
var attestations = await _service.GenerateBatchAsync(replays);
// Assert
attestations.Should().HaveCount(3);
attestations[0].ManifestId.Should().Be("bundle-1");
attestations[1].ManifestId.Should().Be("bundle-2");
attestations[2].ManifestId.Should().Be("bundle-3");
attestations[1].Match.Should().BeFalse();
}
[Fact]
public async Task GenerateAsync_ComputesInputsDigest()
{
// Arrange
var manifest = CreateTestManifest();
var result = CreateTestResult();
// Act
var attestation = await _service.GenerateAsync(manifest, result);
var predicate = attestation.Statement.Predicate;
// Assert
predicate.InputsDigest.Should().StartWith("sha256:");
predicate.InputsDigest.Should().HaveLength(71); // sha256: + 64 hex chars
}
[Fact]
public async Task GenerateAsync_ThrowsForNullManifest()
{
// Arrange
var result = CreateTestResult();
// Act & Assert
await Assert.ThrowsAsync<ArgumentNullException>(
() => _service.GenerateAsync(null!, result));
}
[Fact]
public async Task GenerateAsync_ThrowsForNullResult()
{
// Arrange
var manifest = CreateTestManifest();
// Act & Assert
await Assert.ThrowsAsync<ArgumentNullException>(
() => _service.GenerateAsync(manifest, null!));
}
[Fact]
public async Task VerifyAsync_TamperedPayload_ReturnsInvalid()
{
// Arrange
var manifest = CreateTestManifest();
var result = CreateTestResult();
var attestation = await _service.GenerateAsync(manifest, result);
// Tamper with the envelope payload
var tamperedAttestation = attestation with
{
Envelope = attestation.Envelope! with
{
Payload = Convert.ToBase64String(new byte[] { 1, 2, 3 })
}
};
// Act
var verificationResult = await _service.VerifyAsync(tamperedAttestation);
// Assert
verificationResult.IsValid.Should().BeFalse();
verificationResult.Errors.Should().Contain(e => e.Contains("payload digest"));
}
}

View File

@@ -10,22 +10,14 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="xunit" Version="2.6.6" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\__Libraries\StellaOps.AuditPack\StellaOps.AuditPack.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.AuditPack\StellaOps.AuditPack.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
<Using Include="FluentAssertions" />
</ItemGroup>
</Project>
</Project>