Add property-based tests for SBOM/VEX document ordering and Unicode normalization determinism
- Implement `SbomVexOrderingDeterminismProperties` for testing component list and vulnerability metadata hash consistency. - Create `UnicodeNormalizationDeterminismProperties` to validate NFC normalization and Unicode string handling. - Add project file for `StellaOps.Testing.Determinism.Properties` with necessary dependencies. - Introduce CI/CD template validation tests including YAML syntax checks and documentation content verification. - Create validation script for CI/CD templates ensuring all required files and structures are present.
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// CallGraphExtractorRegistryTests.cs
|
||||
// Sprint: SPRINT_20251226_005_SCANNER_reachability_extractors (REACH-REG-02)
|
||||
// Description: Tests for the call graph extractor registry.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Scanner.CallGraph.DotNet;
|
||||
using StellaOps.Scanner.CallGraph.Go;
|
||||
using StellaOps.Scanner.CallGraph.Java;
|
||||
using StellaOps.Scanner.CallGraph.Node;
|
||||
using StellaOps.Scanner.CallGraph.Python;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace StellaOps.Scanner.CallGraph.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="CallGraphExtractorRegistry"/> ensuring proper registration
|
||||
/// and deterministic behavior across all language extractors.
|
||||
/// </summary>
|
||||
[Trait("Category", "Determinism")]
|
||||
public class CallGraphExtractorRegistryTests
|
||||
{
|
||||
private readonly ITestOutputHelper _output;
|
||||
private readonly FixedTimeProvider _timeProvider;
|
||||
|
||||
public CallGraphExtractorRegistryTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
_timeProvider = new FixedTimeProvider(new DateTimeOffset(2025, 12, 26, 0, 0, 0, TimeSpan.Zero));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Registry_ContainsAllExpectedLanguages()
|
||||
{
|
||||
// Arrange
|
||||
var extractors = CreateAllExtractors();
|
||||
var registry = new CallGraphExtractorRegistry(extractors);
|
||||
|
||||
// Act
|
||||
var languages = registry.SupportedLanguages;
|
||||
|
||||
// Assert
|
||||
Assert.Contains("dotnet", languages);
|
||||
Assert.Contains("go", languages);
|
||||
Assert.Contains("java", languages);
|
||||
Assert.Contains("node", languages);
|
||||
Assert.Contains("python", languages);
|
||||
Assert.Equal(5, languages.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Registry_LanguagesAreOrderedDeterministically()
|
||||
{
|
||||
// Arrange - Create registries with extractors in different orders
|
||||
var extractors1 = CreateAllExtractors().ToList();
|
||||
var extractors2 = CreateAllExtractors().Reverse().ToList();
|
||||
|
||||
var registry1 = new CallGraphExtractorRegistry(extractors1);
|
||||
var registry2 = new CallGraphExtractorRegistry(extractors2);
|
||||
|
||||
// Act
|
||||
var languages1 = registry1.SupportedLanguages;
|
||||
var languages2 = registry2.SupportedLanguages;
|
||||
|
||||
// Assert - Same order regardless of input order
|
||||
Assert.Equal(languages1.Count, languages2.Count);
|
||||
for (int i = 0; i < languages1.Count; i++)
|
||||
{
|
||||
Assert.Equal(languages1[i], languages2[i]);
|
||||
}
|
||||
|
||||
// Verify alphabetical ordering
|
||||
var sorted = languages1.OrderBy(l => l, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
Assert.Equal(sorted, languages1.ToList());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("dotnet")]
|
||||
[InlineData("go")]
|
||||
[InlineData("java")]
|
||||
[InlineData("node")]
|
||||
[InlineData("python")]
|
||||
public void Registry_GetExtractor_ReturnsCorrectExtractor(string language)
|
||||
{
|
||||
// Arrange
|
||||
var extractors = CreateAllExtractors();
|
||||
var registry = new CallGraphExtractorRegistry(extractors);
|
||||
|
||||
// Act
|
||||
var extractor = registry.GetExtractor(language);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(extractor);
|
||||
Assert.Equal(language, extractor.Language, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("JAVA")]
|
||||
[InlineData("Java")]
|
||||
[InlineData("PYTHON")]
|
||||
[InlineData("Python")]
|
||||
[InlineData("NODE")]
|
||||
[InlineData("Node")]
|
||||
public void Registry_GetExtractor_IsCaseInsensitive(string language)
|
||||
{
|
||||
// Arrange
|
||||
var extractors = CreateAllExtractors();
|
||||
var registry = new CallGraphExtractorRegistry(extractors);
|
||||
|
||||
// Act
|
||||
var extractor = registry.GetExtractor(language);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(extractor);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("rust")]
|
||||
[InlineData("ruby")]
|
||||
[InlineData("php")]
|
||||
[InlineData("unknown")]
|
||||
[InlineData("")]
|
||||
[InlineData(null)]
|
||||
public void Registry_GetExtractor_ReturnsNullForUnsupported(string? language)
|
||||
{
|
||||
// Arrange
|
||||
var extractors = CreateAllExtractors();
|
||||
var registry = new CallGraphExtractorRegistry(extractors);
|
||||
|
||||
// Act
|
||||
var extractor = registry.GetExtractor(language!);
|
||||
|
||||
// Assert
|
||||
Assert.Null(extractor);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Registry_IsLanguageSupported_ReturnsCorrectValues()
|
||||
{
|
||||
// Arrange
|
||||
var extractors = CreateAllExtractors();
|
||||
var registry = new CallGraphExtractorRegistry(extractors);
|
||||
|
||||
// Assert - Supported languages
|
||||
Assert.True(registry.IsLanguageSupported("java"));
|
||||
Assert.True(registry.IsLanguageSupported("python"));
|
||||
Assert.True(registry.IsLanguageSupported("node"));
|
||||
Assert.True(registry.IsLanguageSupported("go"));
|
||||
Assert.True(registry.IsLanguageSupported("dotnet"));
|
||||
|
||||
// Assert - Unsupported languages
|
||||
Assert.False(registry.IsLanguageSupported("rust"));
|
||||
Assert.False(registry.IsLanguageSupported(""));
|
||||
Assert.False(registry.IsLanguageSupported(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Registry_DuplicateRegistration_KeepsFirst()
|
||||
{
|
||||
// Arrange - Two extractors for same language
|
||||
var extractor1 = new JavaCallGraphExtractor(
|
||||
NullLogger<JavaCallGraphExtractor>.Instance, _timeProvider);
|
||||
var extractor2 = new JavaCallGraphExtractor(
|
||||
NullLogger<JavaCallGraphExtractor>.Instance, _timeProvider);
|
||||
|
||||
var extractors = new ICallGraphExtractor[] { extractor1, extractor2 };
|
||||
|
||||
// Act
|
||||
var registry = new CallGraphExtractorRegistry(extractors);
|
||||
|
||||
// Assert - Only one Java extractor should be registered
|
||||
var retrieved = registry.GetExtractor("java");
|
||||
Assert.NotNull(retrieved);
|
||||
Assert.Same(extractor1, retrieved);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Registry_EmptyExtractors_HandledGracefully()
|
||||
{
|
||||
// Arrange & Act
|
||||
var registry = new CallGraphExtractorRegistry(Array.Empty<ICallGraphExtractor>());
|
||||
|
||||
// Assert
|
||||
Assert.Empty(registry.SupportedLanguages);
|
||||
Assert.Empty(registry.Extractors);
|
||||
Assert.Null(registry.GetExtractor("java"));
|
||||
Assert.False(registry.IsLanguageSupported("java"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Registry_ExtractorsAreDeterministicallyOrdered()
|
||||
{
|
||||
// Arrange - Create registry multiple times with shuffled input
|
||||
var random = new Random(42); // Fixed seed for reproducibility
|
||||
|
||||
var results = new List<IReadOnlyList<ICallGraphExtractor>>();
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var extractors = CreateAllExtractors()
|
||||
.OrderBy(_ => random.Next())
|
||||
.ToList();
|
||||
var registry = new CallGraphExtractorRegistry(extractors);
|
||||
results.Add(registry.Extractors);
|
||||
}
|
||||
|
||||
// Assert - All registries have same extractor order
|
||||
var first = results[0];
|
||||
foreach (var result in results.Skip(1))
|
||||
{
|
||||
Assert.Equal(first.Count, result.Count);
|
||||
for (int i = 0; i < first.Count; i++)
|
||||
{
|
||||
Assert.Equal(first[i].Language, result[i].Language);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<ICallGraphExtractor> CreateAllExtractors()
|
||||
{
|
||||
yield return new DotNetCallGraphExtractor(
|
||||
NullLogger<DotNetCallGraphExtractor>.Instance, _timeProvider);
|
||||
yield return new GoCallGraphExtractor(
|
||||
NullLogger<GoCallGraphExtractor>.Instance, _timeProvider);
|
||||
yield return new JavaCallGraphExtractor(
|
||||
NullLogger<JavaCallGraphExtractor>.Instance, _timeProvider);
|
||||
yield return new NodeCallGraphExtractor(_timeProvider);
|
||||
yield return new PythonCallGraphExtractor(
|
||||
NullLogger<PythonCallGraphExtractor>.Instance, _timeProvider);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// FuncProofBuilderTests.cs
|
||||
// Sprint: SPRINT_20251226_009_SCANNER_funcproof
|
||||
// Task: FUNC-18
|
||||
// Description: Unit tests for FuncProofBuilder.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using FluentAssertions;
|
||||
using StellaOps.Scanner.Evidence.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Evidence.Tests;
|
||||
|
||||
public sealed class FuncProofBuilderTests
|
||||
{
|
||||
[Fact]
|
||||
public void Build_WithBinaryIdentity_SetsFileProperties()
|
||||
{
|
||||
// Arrange
|
||||
var fileHash = "abc123def456abc123def456abc123def456abc123def456abc123def456abc1";
|
||||
var buildId = "build-12345";
|
||||
var fileSize = 1024L;
|
||||
|
||||
// Act
|
||||
var proof = new FuncProofBuilder()
|
||||
.WithBinaryIdentity(fileHash, buildId, fileSize)
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
proof.FileSha256.Should().Be(fileHash);
|
||||
proof.BuildId.Should().Be(buildId);
|
||||
proof.FileSize.Should().Be(fileSize);
|
||||
proof.SchemaVersion.Should().Be(FuncProofConstants.SchemaVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithSection_AddsSectionToProof()
|
||||
{
|
||||
// Arrange
|
||||
var sectionName = ".text";
|
||||
var sectionOffset = 0x1000UL;
|
||||
var sectionSize = 0x5000UL;
|
||||
var sectionHash = "section_hash_12345";
|
||||
|
||||
// Act
|
||||
var proof = new FuncProofBuilder()
|
||||
.WithBinaryIdentity("filehash", "build", 1024)
|
||||
.AddSection(sectionName, sectionOffset, sectionSize, sectionHash)
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
proof.Sections.Should().HaveCount(1);
|
||||
proof.Sections![0].Name.Should().Be(sectionName);
|
||||
proof.Sections![0].Offset.Should().Be(sectionOffset);
|
||||
proof.Sections![0].Size.Should().Be(sectionSize);
|
||||
proof.Sections![0].Hash.Should().Be(sectionHash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithMultipleSections_AddsAllSections()
|
||||
{
|
||||
// Arrange & Act
|
||||
var proof = new FuncProofBuilder()
|
||||
.WithBinaryIdentity("filehash", "build", 1024)
|
||||
.AddSection(".text", 0x1000, 0x5000, "hash1")
|
||||
.AddSection(".rodata", 0x6000, 0x2000, "hash2")
|
||||
.AddSection(".data", 0x8000, 0x1000, "hash3")
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
proof.Sections.Should().HaveCount(3);
|
||||
proof.Sections![0].Name.Should().Be(".text");
|
||||
proof.Sections![1].Name.Should().Be(".rodata");
|
||||
proof.Sections![2].Name.Should().Be(".data");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithFunction_AddsFunctionToProof()
|
||||
{
|
||||
// Arrange
|
||||
var funcName = "main";
|
||||
var funcOffset = 0x1100UL;
|
||||
var funcSize = 256UL;
|
||||
var symbolDigest = "symbol_digest_abc123";
|
||||
var functionHash = "function_hash_def456";
|
||||
|
||||
// Act
|
||||
var proof = new FuncProofBuilder()
|
||||
.WithBinaryIdentity("filehash", "build", 1024)
|
||||
.AddFunction(funcName, funcOffset, funcSize, symbolDigest, functionHash)
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
proof.Functions.Should().HaveCount(1);
|
||||
proof.Functions![0].Name.Should().Be(funcName);
|
||||
proof.Functions![0].Offset.Should().Be(funcOffset);
|
||||
proof.Functions![0].Size.Should().Be(funcSize);
|
||||
proof.Functions![0].SymbolDigest.Should().Be(symbolDigest);
|
||||
proof.Functions![0].Hash.Should().Be(functionHash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithFunctionCallers_SetsCallersOnFunction()
|
||||
{
|
||||
// Arrange
|
||||
var callers = new List<string> { "caller1", "caller2", "caller3" };
|
||||
|
||||
// Act
|
||||
var proof = new FuncProofBuilder()
|
||||
.WithBinaryIdentity("filehash", "build", 1024)
|
||||
.AddFunction("main", 0x1100, 256, "sym", "hash", callers: callers)
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
proof.Functions![0].Callers.Should().BeEquivalentTo(callers);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithTrace_AddsTraceToProof()
|
||||
{
|
||||
// Arrange
|
||||
var entryFunc = "vulnerable_func";
|
||||
var hops = new List<string> { "main", "process_input", "parse_data", "vulnerable_func" };
|
||||
|
||||
// Act
|
||||
var proof = new FuncProofBuilder()
|
||||
.WithBinaryIdentity("filehash", "build", 1024)
|
||||
.AddTrace(entryFunc, hops, truncated: false)
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
proof.Traces.Should().HaveCount(1);
|
||||
proof.Traces![0].EntryFunction.Should().Be(entryFunc);
|
||||
proof.Traces![0].Hops.Should().BeEquivalentTo(hops);
|
||||
proof.Traces![0].Truncated.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithTruncatedTrace_SetsTruncatedFlag()
|
||||
{
|
||||
// Arrange
|
||||
var hops = Enumerable.Range(0, 15).Select(i => $"func_{i}").ToList();
|
||||
|
||||
// Act
|
||||
var proof = new FuncProofBuilder()
|
||||
.WithBinaryIdentity("filehash", "build", 1024)
|
||||
.AddTrace("target", hops, truncated: true)
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
proof.Traces![0].Truncated.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WithMetadata_SetsMetadataProperties()
|
||||
{
|
||||
// Arrange
|
||||
var tool = "test-tool";
|
||||
var version = "1.0.0";
|
||||
var timestamp = "2024-01-01T00:00:00Z";
|
||||
|
||||
// Act
|
||||
var proof = new FuncProofBuilder()
|
||||
.WithBinaryIdentity("filehash", "build", 1024)
|
||||
.WithMetadata(tool, version, timestamp)
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
proof.Metadata.Should().NotBeNull();
|
||||
proof.Metadata!.Tool.Should().Be(tool);
|
||||
proof.Metadata.ToolVersion.Should().Be(version);
|
||||
proof.Metadata.CreatedAt.Should().Be(timestamp);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_GeneratesProofId()
|
||||
{
|
||||
// Arrange & Act
|
||||
var proof = new FuncProofBuilder()
|
||||
.WithBinaryIdentity("filehash", "build", 1024)
|
||||
.AddFunction("main", 0x1000, 100, "sym", "hash")
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
proof.ProofId.Should().NotBeNullOrEmpty();
|
||||
proof.ProofId.Should().HaveLength(64); // SHA-256 hex
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_SameInput_GeneratesSameProofId()
|
||||
{
|
||||
// Arrange
|
||||
FuncProof BuildProof() => new FuncProofBuilder()
|
||||
.WithBinaryIdentity("filehash", "build", 1024)
|
||||
.AddSection(".text", 0x1000, 0x5000, "section_hash")
|
||||
.AddFunction("main", 0x1100, 256, "sym", "hash")
|
||||
.WithMetadata("tool", "1.0", "2024-01-01T00:00:00Z")
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var proof1 = BuildProof();
|
||||
var proof2 = BuildProof();
|
||||
|
||||
// Assert
|
||||
proof1.ProofId.Should().Be(proof2.ProofId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_DifferentInput_GeneratesDifferentProofId()
|
||||
{
|
||||
// Arrange & Act
|
||||
var proof1 = new FuncProofBuilder()
|
||||
.WithBinaryIdentity("filehash1", "build", 1024)
|
||||
.AddFunction("main", 0x1000, 100, "sym1", "hash1")
|
||||
.Build();
|
||||
|
||||
var proof2 = new FuncProofBuilder()
|
||||
.WithBinaryIdentity("filehash2", "build", 1024)
|
||||
.AddFunction("main", 0x1000, 100, "sym2", "hash2")
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
proof1.ProofId.Should().NotBe(proof2.ProofId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeSymbolDigest_DeterministicForSameInput()
|
||||
{
|
||||
// Arrange
|
||||
var name = "main";
|
||||
var offset = 0x1000UL;
|
||||
|
||||
// Act
|
||||
var digest1 = FuncProofBuilder.ComputeSymbolDigest(name, offset);
|
||||
var digest2 = FuncProofBuilder.ComputeSymbolDigest(name, offset);
|
||||
|
||||
// Assert
|
||||
digest1.Should().Be(digest2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeSymbolDigest_DifferentForDifferentOffset()
|
||||
{
|
||||
// Arrange
|
||||
var name = "main";
|
||||
|
||||
// Act
|
||||
var digest1 = FuncProofBuilder.ComputeSymbolDigest(name, 0x1000);
|
||||
var digest2 = FuncProofBuilder.ComputeSymbolDigest(name, 0x2000);
|
||||
|
||||
// Assert
|
||||
digest1.Should().NotBe(digest2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeFunctionHash_DeterministicForSameInput()
|
||||
{
|
||||
// Arrange
|
||||
var bytes = new byte[] { 0x55, 0x48, 0x89, 0xe5, 0xc3 }; // push rbp; mov rbp, rsp; ret
|
||||
|
||||
// Act
|
||||
var hash1 = FuncProofBuilder.ComputeFunctionHash(bytes);
|
||||
var hash2 = FuncProofBuilder.ComputeFunctionHash(bytes);
|
||||
|
||||
// Assert
|
||||
hash1.Should().Be(hash2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeFunctionHash_DifferentForDifferentInput()
|
||||
{
|
||||
// Arrange
|
||||
var bytes1 = new byte[] { 0x55, 0x48, 0x89, 0xe5, 0xc3 };
|
||||
var bytes2 = new byte[] { 0x55, 0x48, 0x89, 0xe5, 0xc9, 0xc3 }; // includes leave
|
||||
|
||||
// Act
|
||||
var hash1 = FuncProofBuilder.ComputeFunctionHash(bytes1);
|
||||
var hash2 = FuncProofBuilder.ComputeFunctionHash(bytes2);
|
||||
|
||||
// Assert
|
||||
hash1.Should().NotBe(hash2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeProofId_DeterministicForSameProof()
|
||||
{
|
||||
// Arrange
|
||||
var proof = new FuncProofBuilder()
|
||||
.WithBinaryIdentity("filehash", "build", 1024)
|
||||
.AddFunction("main", 0x1000, 100, "sym", "hash")
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var id1 = FuncProofBuilder.ComputeProofId(proof);
|
||||
var id2 = FuncProofBuilder.ComputeProofId(proof);
|
||||
|
||||
// Assert
|
||||
id1.Should().Be(id2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_FunctionOrdering_IsDeterministic()
|
||||
{
|
||||
// Arrange & Act
|
||||
var proof1 = new FuncProofBuilder()
|
||||
.WithBinaryIdentity("filehash", "build", 1024)
|
||||
.AddFunction("func_c", 0x3000, 100, "sym_c", "hash_c")
|
||||
.AddFunction("func_a", 0x1000, 100, "sym_a", "hash_a")
|
||||
.AddFunction("func_b", 0x2000, 100, "sym_b", "hash_b")
|
||||
.Build();
|
||||
|
||||
var proof2 = new FuncProofBuilder()
|
||||
.WithBinaryIdentity("filehash", "build", 1024)
|
||||
.AddFunction("func_b", 0x2000, 100, "sym_b", "hash_b")
|
||||
.AddFunction("func_c", 0x3000, 100, "sym_c", "hash_c")
|
||||
.AddFunction("func_a", 0x1000, 100, "sym_a", "hash_a")
|
||||
.Build();
|
||||
|
||||
// Assert - functions should be sorted by offset for determinism
|
||||
proof1.Functions![0].Name.Should().Be(proof2.Functions![0].Name);
|
||||
proof1.Functions![1].Name.Should().Be(proof2.Functions![1].Name);
|
||||
proof1.Functions![2].Name.Should().Be(proof2.Functions![2].Name);
|
||||
proof1.ProofId.Should().Be(proof2.ProofId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// FuncProofDsseServiceTests.cs
|
||||
// Sprint: SPRINT_20251226_009_SCANNER_funcproof
|
||||
// Task: FUNC-18
|
||||
// Description: Unit tests for FuncProof DSSE signing and verification.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using StellaOps.Replay.Core;
|
||||
using StellaOps.Scanner.Evidence.Models;
|
||||
using StellaOps.Scanner.ProofSpine;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Evidence.Tests;
|
||||
|
||||
public sealed class FuncProofDsseServiceTests
|
||||
{
|
||||
private readonly Mock<IDsseSigningService> _signingServiceMock;
|
||||
private readonly IOptions<FuncProofDsseOptions> _options;
|
||||
private readonly ILogger<FuncProofDsseService> _logger;
|
||||
|
||||
public FuncProofDsseServiceTests()
|
||||
{
|
||||
_signingServiceMock = new Mock<IDsseSigningService>();
|
||||
_options = Options.Create(new FuncProofDsseOptions
|
||||
{
|
||||
KeyId = "test-key-id",
|
||||
Algorithm = "hs256"
|
||||
});
|
||||
_logger = NullLogger<FuncProofDsseService>.Instance;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SignAsync_WithValidProof_ReturnsSignedEnvelope()
|
||||
{
|
||||
// Arrange
|
||||
var proof = CreateTestProof();
|
||||
var expectedEnvelope = new DsseEnvelope(
|
||||
FuncProofConstants.MediaType,
|
||||
Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("{}")),
|
||||
new[] { new DsseSignature("test-key-id", "test-signature") });
|
||||
|
||||
_signingServiceMock
|
||||
.Setup(x => x.SignAsync(
|
||||
It.IsAny<object>(),
|
||||
FuncProofConstants.MediaType,
|
||||
It.IsAny<ICryptoProfile>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(expectedEnvelope);
|
||||
|
||||
var service = new FuncProofDsseService(_signingServiceMock.Object, _options, _logger);
|
||||
|
||||
// Act
|
||||
var result = await service.SignAsync(proof);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.Envelope.Should().Be(expectedEnvelope);
|
||||
result.EnvelopeId.Should().NotBeNullOrEmpty();
|
||||
result.EnvelopeJson.Should().NotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SignAsync_WithNullProofId_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange
|
||||
var proof = new FuncProof
|
||||
{
|
||||
ProofId = null, // Invalid
|
||||
BuildId = "build-123",
|
||||
FileSha256 = "abc123"
|
||||
};
|
||||
|
||||
var service = new FuncProofDsseService(_signingServiceMock.Object, _options, _logger);
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<ArgumentException>(() => service.SignAsync(proof));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SignAsync_CallsSigningServiceWithCorrectPayloadType()
|
||||
{
|
||||
// Arrange
|
||||
var proof = CreateTestProof();
|
||||
var capturedPayloadType = string.Empty;
|
||||
|
||||
_signingServiceMock
|
||||
.Setup(x => x.SignAsync(
|
||||
It.IsAny<object>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<ICryptoProfile>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Callback<object, string, ICryptoProfile, CancellationToken>((_, payloadType, _, _) =>
|
||||
{
|
||||
capturedPayloadType = payloadType;
|
||||
})
|
||||
.ReturnsAsync(CreateTestEnvelope());
|
||||
|
||||
var service = new FuncProofDsseService(_signingServiceMock.Object, _options, _logger);
|
||||
|
||||
// Act
|
||||
await service.SignAsync(proof);
|
||||
|
||||
// Assert
|
||||
capturedPayloadType.Should().Be(FuncProofConstants.MediaType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_WithValidEnvelope_ReturnsSuccessResult()
|
||||
{
|
||||
// Arrange
|
||||
var proof = CreateTestProof();
|
||||
var proofJson = System.Text.Json.JsonSerializer.Serialize(proof);
|
||||
var envelope = new DsseEnvelope(
|
||||
FuncProofConstants.MediaType,
|
||||
Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(proofJson)),
|
||||
new[] { new DsseSignature("test-key-id", "test-signature") });
|
||||
|
||||
_signingServiceMock
|
||||
.Setup(x => x.VerifyAsync(envelope, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new DsseVerificationOutcome(true, true, null));
|
||||
|
||||
var service = new FuncProofDsseService(_signingServiceMock.Object, _options, _logger);
|
||||
|
||||
// Act
|
||||
var result = await service.VerifyAsync(envelope);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeTrue();
|
||||
result.IsTrusted.Should().BeTrue();
|
||||
result.FailureReason.Should().BeNull();
|
||||
result.FuncProof.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_WithWrongPayloadType_ReturnsInvalidResult()
|
||||
{
|
||||
// Arrange
|
||||
var envelope = new DsseEnvelope(
|
||||
"application/wrong-type",
|
||||
Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("{}")),
|
||||
new[] { new DsseSignature("test-key-id", "test-signature") });
|
||||
|
||||
var service = new FuncProofDsseService(_signingServiceMock.Object, _options, _logger);
|
||||
|
||||
// Act
|
||||
var result = await service.VerifyAsync(envelope);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.FailureReason.Should().Contain("Invalid payload type");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_WithFailedSignature_ReturnsInvalidResult()
|
||||
{
|
||||
// Arrange
|
||||
var proof = CreateTestProof();
|
||||
var proofJson = System.Text.Json.JsonSerializer.Serialize(proof);
|
||||
var envelope = new DsseEnvelope(
|
||||
FuncProofConstants.MediaType,
|
||||
Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(proofJson)),
|
||||
new[] { new DsseSignature("test-key-id", "bad-signature") });
|
||||
|
||||
_signingServiceMock
|
||||
.Setup(x => x.VerifyAsync(envelope, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new DsseVerificationOutcome(false, false, "dsse_sig_mismatch"));
|
||||
|
||||
var service = new FuncProofDsseService(_signingServiceMock.Object, _options, _logger);
|
||||
|
||||
// Act
|
||||
var result = await service.VerifyAsync(envelope);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.FailureReason.Should().Be("dsse_sig_mismatch");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtractPayload_WithValidEnvelope_ReturnsFuncProof()
|
||||
{
|
||||
// Arrange
|
||||
var proof = CreateTestProof();
|
||||
var proofJson = System.Text.Json.JsonSerializer.Serialize(proof, new System.Text.Json.JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase
|
||||
});
|
||||
var envelope = new DsseEnvelope(
|
||||
FuncProofConstants.MediaType,
|
||||
Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(proofJson)),
|
||||
Array.Empty<DsseSignature>());
|
||||
|
||||
var service = new FuncProofDsseService(_signingServiceMock.Object, _options, _logger);
|
||||
|
||||
// Act
|
||||
var extracted = service.ExtractPayload(envelope);
|
||||
|
||||
// Assert
|
||||
extracted.Should().NotBeNull();
|
||||
extracted!.ProofId.Should().Be(proof.ProofId);
|
||||
extracted.BuildId.Should().Be(proof.BuildId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtractPayload_WithInvalidBase64_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var envelope = new DsseEnvelope(
|
||||
FuncProofConstants.MediaType,
|
||||
"not-valid-base64!!!",
|
||||
Array.Empty<DsseSignature>());
|
||||
|
||||
var service = new FuncProofDsseService(_signingServiceMock.Object, _options, _logger);
|
||||
|
||||
// Act
|
||||
var extracted = service.ExtractPayload(envelope);
|
||||
|
||||
// Assert
|
||||
extracted.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtractPayload_WithInvalidJson_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var envelope = new DsseEnvelope(
|
||||
FuncProofConstants.MediaType,
|
||||
Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("not-valid-json")),
|
||||
Array.Empty<DsseSignature>());
|
||||
|
||||
var service = new FuncProofDsseService(_signingServiceMock.Object, _options, _logger);
|
||||
|
||||
// Act
|
||||
var extracted = service.ExtractPayload(envelope);
|
||||
|
||||
// Assert
|
||||
extracted.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToUnsignedEnvelope_CreatesValidEnvelope()
|
||||
{
|
||||
// Arrange
|
||||
var proof = CreateTestProof();
|
||||
|
||||
// Act
|
||||
var envelope = proof.ToUnsignedEnvelope();
|
||||
|
||||
// Assert
|
||||
envelope.Should().NotBeNull();
|
||||
envelope.PayloadType.Should().Be(FuncProofConstants.MediaType);
|
||||
envelope.Signatures.Should().BeEmpty();
|
||||
envelope.Payload.Should().NotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseEnvelope_WithValidJson_ReturnsEnvelope()
|
||||
{
|
||||
// Arrange
|
||||
var envelope = new DsseEnvelope(
|
||||
"test-type",
|
||||
"dGVzdA==",
|
||||
new[] { new DsseSignature("key", "sig") });
|
||||
var json = System.Text.Json.JsonSerializer.Serialize(envelope, new System.Text.Json.JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase
|
||||
});
|
||||
|
||||
// Act
|
||||
var parsed = FuncProofDsseExtensions.ParseEnvelope(json);
|
||||
|
||||
// Assert
|
||||
parsed.Should().NotBeNull();
|
||||
parsed!.PayloadType.Should().Be("test-type");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseEnvelope_WithInvalidJson_ReturnsNull()
|
||||
{
|
||||
// Act
|
||||
var parsed = FuncProofDsseExtensions.ParseEnvelope("invalid json {{{");
|
||||
|
||||
// Assert
|
||||
parsed.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseEnvelope_WithEmptyString_ReturnsNull()
|
||||
{
|
||||
// Act
|
||||
var parsed = FuncProofDsseExtensions.ParseEnvelope("");
|
||||
|
||||
// Assert
|
||||
parsed.Should().BeNull();
|
||||
}
|
||||
|
||||
private static FuncProof CreateTestProof()
|
||||
{
|
||||
return new FuncProofBuilder()
|
||||
.WithBinaryIdentity(
|
||||
"abc123def456abc123def456abc123def456abc123def456abc123def456abc1",
|
||||
"build-123",
|
||||
1024)
|
||||
.AddSection(".text", 0x1000, 0x5000, "section_hash")
|
||||
.AddFunction("main", 0x1100, 256, "sym_main", "func_hash_main")
|
||||
.WithMetadata("test-tool", "1.0.0", "2024-01-01T00:00:00Z")
|
||||
.Build();
|
||||
}
|
||||
|
||||
private static DsseEnvelope CreateTestEnvelope()
|
||||
{
|
||||
return new DsseEnvelope(
|
||||
FuncProofConstants.MediaType,
|
||||
Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("{}")),
|
||||
new[] { new DsseSignature("test-key-id", "test-signature") });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// SbomFuncProofLinkerTests.cs
|
||||
// Sprint: SPRINT_20251226_009_SCANNER_funcproof
|
||||
// Task: FUNC-15 — SBOM evidence link unit tests
|
||||
// Description: Tests for SBOM-FuncProof linking functionality.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Scanner.Evidence;
|
||||
using StellaOps.Scanner.Evidence.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Evidence.Tests;
|
||||
|
||||
public sealed class SbomFuncProofLinkerTests
|
||||
{
|
||||
private readonly SbomFuncProofLinker _linker;
|
||||
|
||||
public SbomFuncProofLinkerTests()
|
||||
{
|
||||
_linker = new SbomFuncProofLinker(NullLogger<SbomFuncProofLinker>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LinkFuncProofEvidence_AddsEvidenceToComponent()
|
||||
{
|
||||
// Arrange
|
||||
var sbom = CreateMinimalCycloneDxSbom("pkg:npm/lodash@4.17.21", "component-1");
|
||||
var funcProof = CreateTestFuncProof();
|
||||
|
||||
// Act
|
||||
var result = _linker.LinkFuncProofEvidence(
|
||||
sbom,
|
||||
"component-1",
|
||||
funcProof,
|
||||
"sha256:abc123",
|
||||
"oci://registry.example.com/proofs:funcproof-test");
|
||||
|
||||
// Assert
|
||||
var doc = JsonNode.Parse(result) as JsonObject;
|
||||
doc.Should().NotBeNull();
|
||||
|
||||
var component = (doc!["components"] as JsonArray)?[0] as JsonObject;
|
||||
component.Should().NotBeNull();
|
||||
|
||||
var evidence = component!["evidence"] as JsonObject;
|
||||
evidence.Should().NotBeNull();
|
||||
|
||||
var callflow = evidence!["callflow"] as JsonObject;
|
||||
callflow.Should().NotBeNull();
|
||||
|
||||
var frames = callflow!["frames"] as JsonArray;
|
||||
frames.Should().NotBeNull();
|
||||
frames!.Count.Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LinkFuncProofEvidence_AddsExternalReference()
|
||||
{
|
||||
// Arrange
|
||||
var sbom = CreateMinimalCycloneDxSbom("pkg:npm/lodash@4.17.21", "component-1");
|
||||
var funcProof = CreateTestFuncProof();
|
||||
|
||||
// Act
|
||||
var result = _linker.LinkFuncProofEvidence(
|
||||
sbom,
|
||||
"component-1",
|
||||
funcProof,
|
||||
"sha256:abc123",
|
||||
"oci://registry.example.com/proofs:funcproof-test");
|
||||
|
||||
// Assert
|
||||
var doc = JsonNode.Parse(result) as JsonObject;
|
||||
var component = (doc!["components"] as JsonArray)?[0] as JsonObject;
|
||||
var externalRefs = component!["externalReferences"] as JsonArray;
|
||||
|
||||
externalRefs.Should().NotBeNull();
|
||||
externalRefs!.Count.Should().BeGreaterThan(0);
|
||||
|
||||
var evidenceRef = externalRefs[0] as JsonObject;
|
||||
evidenceRef!["type"]!.GetValue<string>().Should().Be("evidence");
|
||||
evidenceRef["url"]!.GetValue<string>().Should().Contain("oci://");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LinkFuncProofEvidence_ThrowsForNonCycloneDx()
|
||||
{
|
||||
// Arrange
|
||||
var spdxSbom = """
|
||||
{
|
||||
"spdxVersion": "SPDX-2.3",
|
||||
"dataLicense": "CC0-1.0",
|
||||
"packages": []
|
||||
}
|
||||
""";
|
||||
var funcProof = CreateTestFuncProof();
|
||||
|
||||
// Act & Assert
|
||||
var act = () => _linker.LinkFuncProofEvidence(
|
||||
spdxSbom,
|
||||
"component-1",
|
||||
funcProof,
|
||||
"sha256:abc123",
|
||||
"oci://test");
|
||||
|
||||
act.Should().Throw<ArgumentException>()
|
||||
.WithMessage("*CycloneDX*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LinkFuncProofEvidence_ThrowsForMissingComponent()
|
||||
{
|
||||
// Arrange
|
||||
var sbom = CreateMinimalCycloneDxSbom("pkg:npm/lodash@4.17.21", "component-1");
|
||||
var funcProof = CreateTestFuncProof();
|
||||
|
||||
// Act & Assert
|
||||
var act = () => _linker.LinkFuncProofEvidence(
|
||||
sbom,
|
||||
"nonexistent-component",
|
||||
funcProof,
|
||||
"sha256:abc123",
|
||||
"oci://test");
|
||||
|
||||
act.Should().Throw<ArgumentException>()
|
||||
.WithMessage("*not found*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtractFuncProofReferences_ReturnsEmptyForNoEvidence()
|
||||
{
|
||||
// Arrange
|
||||
var sbom = CreateMinimalCycloneDxSbom("pkg:npm/lodash@4.17.21", "component-1");
|
||||
|
||||
// Act
|
||||
var refs = _linker.ExtractFuncProofReferences(sbom, "component-1");
|
||||
|
||||
// Assert
|
||||
refs.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtractFuncProofReferences_FindsLinkedEvidence()
|
||||
{
|
||||
// Arrange
|
||||
var sbom = CreateMinimalCycloneDxSbom("pkg:npm/lodash@4.17.21", "component-1");
|
||||
var funcProof = CreateTestFuncProof();
|
||||
|
||||
var linkedSbom = _linker.LinkFuncProofEvidence(
|
||||
sbom,
|
||||
"component-1",
|
||||
funcProof,
|
||||
"sha256:abc123def456",
|
||||
"oci://registry.example.com/proofs:funcproof-v1");
|
||||
|
||||
// Act
|
||||
var refs = _linker.ExtractFuncProofReferences(linkedSbom, "component-1");
|
||||
|
||||
// Assert
|
||||
refs.Should().HaveCount(1);
|
||||
refs[0].ProofId.Should().Be(funcProof.ProofId);
|
||||
refs[0].BuildId.Should().Be(funcProof.BuildId);
|
||||
refs[0].FunctionCount.Should().Be(funcProof.Functions.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateEvidenceRef_PopulatesAllFields()
|
||||
{
|
||||
// Arrange
|
||||
var funcProof = CreateTestFuncProof();
|
||||
|
||||
// Act
|
||||
var evidenceRef = _linker.CreateEvidenceRef(
|
||||
funcProof,
|
||||
"sha256:proof-digest-123",
|
||||
"oci://registry/proof:v1");
|
||||
|
||||
// Assert
|
||||
evidenceRef.ProofId.Should().Be(funcProof.ProofId);
|
||||
evidenceRef.BuildId.Should().Be(funcProof.BuildId);
|
||||
evidenceRef.FileSha256.Should().Be(funcProof.FileSha256);
|
||||
evidenceRef.ProofDigest.Should().Be("sha256:proof-digest-123");
|
||||
evidenceRef.Location.Should().Be("oci://registry/proof:v1");
|
||||
evidenceRef.FunctionCount.Should().Be(2);
|
||||
evidenceRef.TraceCount.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LinkFuncProofEvidence_IncludesProofProperties()
|
||||
{
|
||||
// Arrange
|
||||
var sbom = CreateMinimalCycloneDxSbom("pkg:npm/lodash@4.17.21", "component-1");
|
||||
var funcProof = CreateTestFuncProof();
|
||||
|
||||
// Act
|
||||
var result = _linker.LinkFuncProofEvidence(
|
||||
sbom,
|
||||
"component-1",
|
||||
funcProof,
|
||||
"sha256:abc123",
|
||||
"oci://registry.example.com/proofs:funcproof-test");
|
||||
|
||||
// Assert
|
||||
var doc = JsonNode.Parse(result) as JsonObject;
|
||||
var component = (doc!["components"] as JsonArray)?[0] as JsonObject;
|
||||
var evidence = component!["evidence"] as JsonObject;
|
||||
var frames = (evidence!["callflow"] as JsonObject)!["frames"] as JsonArray;
|
||||
var properties = (frames![0] as JsonObject)!["properties"] as JsonArray;
|
||||
|
||||
properties.Should().NotBeNull();
|
||||
|
||||
var typeProperty = properties!.OfType<JsonObject>()
|
||||
.FirstOrDefault(p => p["name"]?.GetValue<string>() == "stellaops:evidence:type");
|
||||
typeProperty.Should().NotBeNull();
|
||||
typeProperty!["value"]!.GetValue<string>().Should().Be("funcproof");
|
||||
|
||||
var proofIdProperty = properties.OfType<JsonObject>()
|
||||
.FirstOrDefault(p => p["name"]?.GetValue<string>() == "stellaops:funcproof:proofId");
|
||||
proofIdProperty.Should().NotBeNull();
|
||||
proofIdProperty!["value"]!.GetValue<string>().Should().Be(funcProof.ProofId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LinkFuncProofEvidence_MergesWithExistingEvidence()
|
||||
{
|
||||
// Arrange
|
||||
var sbom = CreateMinimalCycloneDxSbom("pkg:npm/lodash@4.17.21", "component-1");
|
||||
var funcProof1 = CreateTestFuncProof("proof-1", "build-1");
|
||||
var funcProof2 = CreateTestFuncProof("proof-2", "build-2");
|
||||
|
||||
// Link first proof
|
||||
var linkedSbom = _linker.LinkFuncProofEvidence(
|
||||
sbom,
|
||||
"component-1",
|
||||
funcProof1,
|
||||
"sha256:digest1",
|
||||
"oci://registry/proof1:v1");
|
||||
|
||||
// Act - Link second proof
|
||||
var result = _linker.LinkFuncProofEvidence(
|
||||
linkedSbom,
|
||||
"component-1",
|
||||
funcProof2,
|
||||
"sha256:digest2",
|
||||
"oci://registry/proof2:v1");
|
||||
|
||||
// Assert
|
||||
var refs = _linker.ExtractFuncProofReferences(result, "component-1");
|
||||
refs.Should().HaveCount(2);
|
||||
refs.Select(r => r.ProofId).Should().Contain("proof-1");
|
||||
refs.Select(r => r.ProofId).Should().Contain("proof-2");
|
||||
}
|
||||
|
||||
private static string CreateMinimalCycloneDxSbom(string purl, string bomRef)
|
||||
{
|
||||
return $$"""
|
||||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.6",
|
||||
"version": 1,
|
||||
"serialNumber": "urn:uuid:{{Guid.NewGuid()}}",
|
||||
"components": [
|
||||
{
|
||||
"type": "library",
|
||||
"bom-ref": "{{bomRef}}",
|
||||
"purl": "{{purl}}",
|
||||
"name": "test-component",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
}
|
||||
|
||||
private static FuncProof CreateTestFuncProof(
|
||||
string? proofId = null,
|
||||
string? buildId = null)
|
||||
{
|
||||
proofId ??= "graph:test-proof-123";
|
||||
buildId ??= "gnu-build-id-abc123";
|
||||
|
||||
return new FuncProof
|
||||
{
|
||||
ProofId = proofId,
|
||||
SchemaVersion = FuncProofConstants.SchemaVersion,
|
||||
BuildId = buildId,
|
||||
BuildIdType = "gnu-build-id",
|
||||
FileSha256 = "abc123def456789",
|
||||
BinaryFormat = "elf",
|
||||
Architecture = "x86_64",
|
||||
IsStripped = false,
|
||||
Sections = ImmutableDictionary<string, FuncProofSection>.Empty.Add(
|
||||
".text",
|
||||
new FuncProofSection
|
||||
{
|
||||
Hash = "blake3:section-hash-123",
|
||||
Offset = 0x1000,
|
||||
Size = 0x5000,
|
||||
VirtualAddress = 0x401000
|
||||
}),
|
||||
Functions = ImmutableArray.Create(
|
||||
new FuncProofFunction
|
||||
{
|
||||
SymbolDigest = "blake3:func1-digest",
|
||||
Symbol = "main",
|
||||
MangledName = "main",
|
||||
Start = "0x401000",
|
||||
End = "0x401100",
|
||||
Size = 256,
|
||||
FunctionHash = "blake3:func1-hash",
|
||||
Confidence = 1.0,
|
||||
DetectionMethod = "dwarf"
|
||||
},
|
||||
new FuncProofFunction
|
||||
{
|
||||
SymbolDigest = "blake3:func2-digest",
|
||||
Symbol = "helper",
|
||||
MangledName = "_Z6helperv",
|
||||
Start = "0x401100",
|
||||
End = "0x401200",
|
||||
Size = 256,
|
||||
FunctionHash = "blake3:func2-hash",
|
||||
Confidence = 0.8,
|
||||
DetectionMethod = "symbol"
|
||||
}),
|
||||
Traces = ImmutableArray.Create(
|
||||
new FuncProofTrace
|
||||
{
|
||||
TraceId = "trace-1",
|
||||
EdgeListHash = "blake3:edge-hash-1",
|
||||
HopCount = 1,
|
||||
EntrySymbolDigest = "blake3:func1-digest",
|
||||
SinkSymbolDigest = "blake3:func2-digest",
|
||||
Path = ImmutableArray.Create("blake3:func1-digest", "blake3:func2-digest"),
|
||||
Truncated = false
|
||||
}),
|
||||
Metadata = new FuncProofMetadata
|
||||
{
|
||||
Generator = "StellaOps.Scanner",
|
||||
GeneratorVersion = "1.0.0",
|
||||
Timestamp = DateTimeOffset.UtcNow.ToString("O"),
|
||||
Properties = ImmutableDictionary<string, string>.Empty
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,9 @@
|
||||
</PackageReference>
|
||||
<PackageReference Include="FluentAssertions" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
@@ -24,5 +26,6 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Evidence/StellaOps.Scanner.Evidence.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.ProofSpine/StellaOps.Scanner.ProofSpine.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user