Add Canonical JSON serialization library with tests and documentation

- Implemented CanonJson class for deterministic JSON serialization and hashing.
- Added unit tests for CanonJson functionality, covering various scenarios including key sorting, handling of nested objects, arrays, and special characters.
- Created project files for the Canonical JSON library and its tests, including necessary package references.
- Added README.md for library usage and API reference.
- Introduced RabbitMqIntegrationFactAttribute for conditional RabbitMQ integration tests.
This commit is contained in:
master
2025-12-19 15:35:00 +02:00
parent 43882078a4
commit 951a38d561
192 changed files with 27550 additions and 2611 deletions

View File

@@ -0,0 +1,344 @@
// -----------------------------------------------------------------------------
// DriftAttestationServiceTests.cs
// Sprint: SPRINT_3600_0004_0001_ui_evidence_chain
// Task: UI-018
// Description: Unit tests for DriftAttestationService.
// -----------------------------------------------------------------------------
using System.Collections.Immutable;
using System.Text.Json;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Time.Testing;
using Moq;
using StellaOps.Scanner.Reachability;
using StellaOps.Scanner.ReachabilityDrift.Attestation;
using Xunit;
namespace StellaOps.Scanner.ReachabilityDrift.Tests;
public sealed class DriftAttestationServiceTests
{
private readonly FakeTimeProvider _timeProvider;
private readonly Mock<IOptionsMonitor<DriftAttestationOptions>> _optionsMock;
private readonly DriftAttestationOptions _options;
public DriftAttestationServiceTests()
{
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 12, 19, 12, 0, 0, TimeSpan.Zero));
_options = new DriftAttestationOptions { Enabled = true, UseSignerService = false };
_optionsMock = new Mock<IOptionsMonitor<DriftAttestationOptions>>();
_optionsMock.Setup(x => x.CurrentValue).Returns(_options);
}
[Fact]
public async Task CreateAttestationAsync_Creates_Valid_Attestation()
{
// Arrange
var service = CreateService();
var request = CreateValidRequest();
// Act
var result = await service.CreateAttestationAsync(request);
// Assert
result.Success.Should().BeTrue();
result.AttestationDigest.Should().StartWith("sha256:");
result.EnvelopeJson.Should().NotBeNullOrEmpty();
result.KeyId.Should().Be("local-dev-key");
result.CreatedAt.Should().Be(_timeProvider.GetUtcNow());
}
[Fact]
public async Task CreateAttestationAsync_Returns_Failure_When_Disabled()
{
// Arrange
_options.Enabled = false;
var service = CreateService();
var request = CreateValidRequest();
// Act
var result = await service.CreateAttestationAsync(request);
// Assert
result.Success.Should().BeFalse();
result.Error.Should().Contain("disabled");
}
[Fact]
public async Task CreateAttestationAsync_Throws_When_Request_Null()
{
// Arrange
var service = CreateService();
// Act & Assert
await Assert.ThrowsAsync<ArgumentNullException>(
() => service.CreateAttestationAsync(null!));
}
[Fact]
public async Task CreateAttestationAsync_Envelope_Contains_Correct_PayloadType()
{
// Arrange
var service = CreateService();
var request = CreateValidRequest();
// Act
var result = await service.CreateAttestationAsync(request);
// Assert
result.EnvelopeJson.Should().Contain("application/vnd.in-toto+json");
}
[Fact]
public async Task CreateAttestationAsync_Envelope_Contains_Signature()
{
// Arrange
var service = CreateService();
var request = CreateValidRequest();
// Act
var result = await service.CreateAttestationAsync(request);
// Assert
var envelope = JsonDocument.Parse(result.EnvelopeJson!);
var signatures = envelope.RootElement.GetProperty("signatures");
signatures.GetArrayLength().Should().Be(1);
signatures[0].GetProperty("keyid").GetString().Should().Be("local-dev-key");
signatures[0].GetProperty("sig").GetString().Should().NotBeNullOrEmpty();
}
[Fact]
public async Task CreateAttestationAsync_Statement_Contains_Predicate()
{
// Arrange
var service = CreateService();
var request = CreateValidRequest();
// Act
var result = await service.CreateAttestationAsync(request);
// Assert
var envelope = JsonDocument.Parse(result.EnvelopeJson!);
var payloadBase64 = envelope.RootElement.GetProperty("payload").GetString();
var payloadBytes = Convert.FromBase64String(payloadBase64!);
var statement = JsonDocument.Parse(payloadBytes);
statement.RootElement.GetProperty("predicateType").GetString()
.Should().Be("stellaops.dev/predicates/reachability-drift@v1");
}
[Fact]
public async Task CreateAttestationAsync_Predicate_Contains_Drift_Summary()
{
// Arrange
var service = CreateService();
var request = CreateValidRequest();
// Act
var result = await service.CreateAttestationAsync(request);
// Assert
var predicate = ExtractPredicate(result.EnvelopeJson!);
predicate.GetProperty("drift").GetProperty("newlyReachableCount").GetInt32().Should().Be(1);
predicate.GetProperty("drift").GetProperty("newlyUnreachableCount").GetInt32().Should().Be(0);
}
[Fact]
public async Task CreateAttestationAsync_Predicate_Contains_Image_References()
{
// Arrange
var service = CreateService();
var request = CreateValidRequest();
// Act
var result = await service.CreateAttestationAsync(request);
// Assert
var predicate = ExtractPredicate(result.EnvelopeJson!);
predicate.GetProperty("baseImage").GetProperty("name").GetString()
.Should().Be("myregistry/myapp");
predicate.GetProperty("baseImage").GetProperty("digest").GetString()
.Should().Be("sha256:base123");
predicate.GetProperty("targetImage").GetProperty("name").GetString()
.Should().Be("myregistry/myapp");
predicate.GetProperty("targetImage").GetProperty("digest").GetString()
.Should().Be("sha256:head456");
}
[Fact]
public async Task CreateAttestationAsync_Predicate_Contains_Analysis_Metadata()
{
// Arrange
var service = CreateService();
var request = CreateValidRequest();
// Act
var result = await service.CreateAttestationAsync(request);
// Assert
var predicate = ExtractPredicate(result.EnvelopeJson!);
var analysis = predicate.GetProperty("analysis");
analysis.GetProperty("baseGraphDigest").GetString().Should().Be("sha256:graph-base");
analysis.GetProperty("headGraphDigest").GetString().Should().Be("sha256:graph-head");
analysis.GetProperty("scanner").GetProperty("name").GetString().Should().Be("StellaOps.Scanner");
}
[Fact]
public async Task CreateAttestationAsync_Produces_Deterministic_Digest_For_Same_Input()
{
// Arrange
var service = CreateService();
var request = CreateValidRequest();
// Act
var result1 = await service.CreateAttestationAsync(request);
var result2 = await service.CreateAttestationAsync(request);
// Assert
result1.AttestationDigest.Should().Be(result2.AttestationDigest);
}
[Fact]
public async Task CreateAttestationAsync_With_Signer_Service_Calls_SignAsync()
{
// Arrange
_options.UseSignerService = true;
var signerMock = new Mock<IDriftSignerClient>();
signerMock.Setup(x => x.SignAsync(It.IsAny<DriftSignerRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new DriftSignerResult
{
Success = true,
Signature = "base64-signature",
KeyId = "test-key-id"
});
var service = CreateService(signerMock.Object);
var request = CreateValidRequest();
// Act
var result = await service.CreateAttestationAsync(request);
// Assert
result.Success.Should().BeTrue();
result.KeyId.Should().Be("test-key-id");
signerMock.Verify(x => x.SignAsync(
It.Is<DriftSignerRequest>(r => r.TenantId == "tenant-1"),
It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task CreateAttestationAsync_Returns_Failure_When_Signer_Fails()
{
// Arrange
_options.UseSignerService = true;
var signerMock = new Mock<IDriftSignerClient>();
signerMock.Setup(x => x.SignAsync(It.IsAny<DriftSignerRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new DriftSignerResult
{
Success = false,
Error = "Key not found"
});
var service = CreateService(signerMock.Object);
var request = CreateValidRequest();
// Act
var result = await service.CreateAttestationAsync(request);
// Assert
result.Success.Should().BeFalse();
result.Error.Should().Contain("Key not found");
}
private DriftAttestationService CreateService(IDriftSignerClient? signerClient = null)
{
return new DriftAttestationService(
signerClient,
_optionsMock.Object,
_timeProvider,
NullLogger<DriftAttestationService>.Instance);
}
private DriftAttestationRequest CreateValidRequest()
{
var driftResult = new ReachabilityDriftResult
{
Id = Guid.NewGuid(),
BaseScanId = "scan-base-123",
HeadScanId = "scan-head-456",
Language = "csharp",
DetectedAt = _timeProvider.GetUtcNow(),
NewlyReachable = ImmutableArray.Create(CreateDriftedSink()),
NewlyUnreachable = ImmutableArray<DriftedSink>.Empty,
ResultDigest = "sha256:result-digest"
};
return new DriftAttestationRequest
{
TenantId = "tenant-1",
DriftResult = driftResult,
BaseImage = new ImageRef
{
Name = "myregistry/myapp",
Digest = "sha256:base123",
Tag = "v1.0.0"
},
TargetImage = new ImageRef
{
Name = "myregistry/myapp",
Digest = "sha256:head456",
Tag = "v1.1.0"
},
BaseGraphDigest = "sha256:graph-base",
HeadGraphDigest = "sha256:graph-head"
};
}
private static DriftedSink CreateDriftedSink()
{
return new DriftedSink
{
Id = Guid.NewGuid(),
SinkNodeId = "sink-node-1",
Symbol = "SqlCommand.ExecuteNonQuery",
SinkCategory = SinkCategory.SqlInjection,
Direction = DriftDirection.BecameReachable,
Cause = new DriftCause
{
Kind = DriftCauseKind.GuardRemoved,
Description = "Security guard was removed from the call path"
},
Path = new CompressedPath
{
Entrypoint = new PathNode
{
NodeId = "entry-1",
Symbol = "Program.Main",
IsChanged = false
},
Sink = new PathNode
{
NodeId = "sink-1",
Symbol = "SqlCommand.ExecuteNonQuery",
IsChanged = false
},
KeyNodes = ImmutableArray<PathNode>.Empty,
IntermediateCount = 3
}
};
}
private static JsonElement ExtractPredicate(string envelopeJson)
{
var envelope = JsonDocument.Parse(envelopeJson);
var payloadBase64 = envelope.RootElement.GetProperty("payload").GetString();
var payloadBytes = Convert.FromBase64String(payloadBase64!);
var statement = JsonDocument.Parse(payloadBytes);
return statement.RootElement.GetProperty("predicate");
}
}

View File

@@ -12,6 +12,10 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="FluentAssertions" Version="7.2.0" />
<PackageReference Include="Microsoft.Extensions.Time.Testing" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0" />
</ItemGroup>
<ItemGroup>