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,258 @@
// -----------------------------------------------------------------------------
// NativeUnknownClassifierTests.cs
// Sprint: SPRINT_3500_0013_0001_native_unknowns
// Task: NUC-005
// Description: Unit tests for NativeUnknownClassifier service.
// -----------------------------------------------------------------------------
using FluentAssertions;
using Microsoft.Extensions.Time.Testing;
using StellaOps.Unknowns.Core.Models;
using StellaOps.Unknowns.Core.Services;
using Xunit;
namespace StellaOps.Unknowns.Core.Tests.Services;
public sealed class NativeUnknownClassifierTests
{
private readonly FakeTimeProvider _timeProvider;
private readonly NativeUnknownClassifier _classifier;
public NativeUnknownClassifierTests()
{
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 12, 19, 12, 0, 0, TimeSpan.Zero));
_classifier = new NativeUnknownClassifier(_timeProvider, "test-classifier");
}
[Fact]
public void ClassifyMissingBuildId_Creates_Unknown_With_Correct_Properties()
{
// Arrange
var context = new NativeUnknownContext
{
Format = "elf",
FilePath = "/usr/lib/libfoo.so.1",
Architecture = "x86_64",
LayerDigest = "sha256:abc123",
LayerIndex = 2,
FileDigest = "sha256:def456",
FileSize = 1024000
};
// Act
var unknown = _classifier.ClassifyMissingBuildId("tenant-1", context);
// Assert
unknown.Should().NotBeNull();
unknown.Kind.Should().Be(UnknownKind.MissingBuildId);
unknown.SubjectType.Should().Be(UnknownSubjectType.Binary);
unknown.SubjectRef.Should().Be("/usr/lib/libfoo.so.1");
unknown.TenantId.Should().Be("tenant-1");
unknown.Severity.Should().Be(UnknownSeverity.Medium);
unknown.CreatedBy.Should().Be("test-classifier");
unknown.ValidFrom.Should().Be(_timeProvider.GetUtcNow());
unknown.SysFrom.Should().Be(_timeProvider.GetUtcNow());
unknown.Context.Should().NotBeNull();
}
[Fact]
public void ClassifyUnknownBuildId_Creates_Unknown_With_BuildId_Reference()
{
// Arrange
var context = new NativeUnknownContext
{
Format = "elf",
FilePath = "/usr/lib/libbar.so.2",
BuildId = "gnu-build-id:abc123def456",
Architecture = "aarch64",
LayerDigest = "sha256:xyz789"
};
// Act
var unknown = _classifier.ClassifyUnknownBuildId("tenant-2", context);
// Assert
unknown.Should().NotBeNull();
unknown.Kind.Should().Be(UnknownKind.UnknownBuildId);
unknown.SubjectRef.Should().Be("gnu-build-id:abc123def456");
unknown.Severity.Should().Be(UnknownSeverity.Low);
}
[Fact]
public void ClassifyUnknownBuildId_Throws_When_BuildId_Missing()
{
// Arrange
var context = new NativeUnknownContext
{
Format = "elf",
FilePath = "/usr/lib/libfoo.so"
};
// Act & Assert
var act = () => _classifier.ClassifyUnknownBuildId("tenant-1", context);
act.Should().Throw<ArgumentException>()
.WithMessage("*BuildId*");
}
[Fact]
public void ClassifyUnresolvedLibrary_Creates_Unknown_With_Import_Info()
{
// Arrange
var context = new NativeUnknownContext
{
Format = "elf",
FilePath = "/usr/bin/myapp",
UnresolvedImport = "libcrypto.so.1.1",
Architecture = "x86_64"
};
// Act
var unknown = _classifier.ClassifyUnresolvedLibrary("tenant-3", context);
// Assert
unknown.Should().NotBeNull();
unknown.Kind.Should().Be(UnknownKind.UnresolvedNativeLibrary);
unknown.SubjectRef.Should().Contain("libcrypto.so.1.1");
unknown.Severity.Should().Be(UnknownSeverity.Medium);
}
[Fact]
public void ClassifyHeuristicDependency_Creates_Unknown_With_Confidence()
{
// Arrange
var context = new NativeUnknownContext
{
Format = "elf",
FilePath = "/usr/bin/dynamic-loader",
HeuristicPattern = "dlopen(\"libplugin-%s.so\", RTLD_NOW)",
HeuristicConfidence = 0.75,
Architecture = "x86_64"
};
// Act
var unknown = _classifier.ClassifyHeuristicDependency("tenant-4", context);
// Assert
unknown.Should().NotBeNull();
unknown.Kind.Should().Be(UnknownKind.HeuristicDependency);
unknown.Severity.Should().Be(UnknownSeverity.Low);
}
[Fact]
public void ClassifyUnsupportedFormat_Creates_Unknown_With_Reason()
{
// Arrange
var context = new NativeUnknownContext
{
Format = "pe",
FilePath = "C:\\Windows\\System32\\legacy.dll",
UnsupportedReason = "PE/COFF format with non-standard overlay",
Architecture = "i686"
};
// Act
var unknown = _classifier.ClassifyUnsupportedFormat("tenant-5", context);
// Assert
unknown.Should().NotBeNull();
unknown.Kind.Should().Be(UnknownKind.UnsupportedBinaryFormat);
unknown.Severity.Should().Be(UnknownSeverity.Info);
}
[Fact]
public void All_Classifications_Have_Unique_Subject_Hashes()
{
// Arrange
var context1 = new NativeUnknownContext { Format = "elf", FilePath = "/lib/a.so", LayerDigest = "sha256:layer1" };
var context2 = new NativeUnknownContext { Format = "elf", FilePath = "/lib/b.so", LayerDigest = "sha256:layer1" };
var context3 = new NativeUnknownContext { Format = "elf", FilePath = "/lib/a.so", LayerDigest = "sha256:layer2" };
// Act
var unknown1 = _classifier.ClassifyMissingBuildId("tenant", context1);
var unknown2 = _classifier.ClassifyMissingBuildId("tenant", context2);
var unknown3 = _classifier.ClassifyMissingBuildId("tenant", context3);
// Assert - Different files or layers should produce different hashes
unknown1.SubjectHash.Should().NotBe(unknown2.SubjectHash);
unknown1.SubjectHash.Should().NotBe(unknown3.SubjectHash);
}
[Fact]
public void Same_Binary_Produces_Same_Subject_Hash()
{
// Arrange - Same file path and layer
var context1 = new NativeUnknownContext { Format = "elf", FilePath = "/lib/same.so", LayerDigest = "sha256:samelayer" };
var context2 = new NativeUnknownContext { Format = "elf", FilePath = "/lib/same.so", LayerDigest = "sha256:samelayer" };
// Act
var unknown1 = _classifier.ClassifyMissingBuildId("tenant", context1);
var unknown2 = _classifier.ClassifyMissingBuildId("tenant", context2);
// Assert - Same file+layer should produce same hash (for deduplication)
unknown1.SubjectHash.Should().Be(unknown2.SubjectHash);
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void ClassifyMissingBuildId_Throws_When_TenantId_Invalid(string? tenantId)
{
// Arrange
var context = new NativeUnknownContext { Format = "elf", FilePath = "/lib/foo.so" };
// Act & Assert
var act = () => _classifier.ClassifyMissingBuildId(tenantId!, context);
act.Should().Throw<ArgumentException>();
}
[Fact]
public void ClassifyMissingBuildId_Throws_When_Context_Null()
{
// Act & Assert
var act = () => _classifier.ClassifyMissingBuildId("tenant", null!);
act.Should().Throw<ArgumentNullException>();
}
[Fact]
public void Constructor_Throws_When_TimeProvider_Null()
{
// Act & Assert
var act = () => new NativeUnknownClassifier(null!, "test");
act.Should().Throw<ArgumentNullException>();
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void Constructor_Throws_When_CreatedBy_Invalid(string? createdBy)
{
// Act & Assert
var act = () => new NativeUnknownClassifier(TimeProvider.System, createdBy!);
act.Should().Throw<ArgumentException>();
}
[Fact]
public void Context_Is_Serialized_To_JsonDocument()
{
// Arrange
var context = new NativeUnknownContext
{
Format = "macho",
FilePath = "/Applications/MyApp.app/Contents/MacOS/MyApp",
BuildId = "macho-uuid:12345678-1234-5678-9abc-def012345678",
Architecture = "arm64"
};
// Act
var unknown = _classifier.ClassifyUnknownBuildId("tenant", context);
// Assert
unknown.Context.Should().NotBeNull();
var root = unknown.Context!.RootElement;
root.GetProperty("format").GetString().Should().Be("macho");
root.GetProperty("filePath").GetString().Should().Contain("MyApp");
root.GetProperty("architecture").GetString().Should().Be("arm64");
}
}

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" ?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<RootNamespace>StellaOps.Unknowns.Core.Tests</RootNamespace>
</PropertyGroup>
<ItemGroup>
<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">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="FluentAssertions" Version="7.2.0" />
<PackageReference Include="Microsoft.Extensions.Time.Testing" Version="10.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../__Libraries/StellaOps.Unknowns.Core/StellaOps.Unknowns.Core.csproj" />
</ItemGroup>
</Project>