save work
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Scanner.Analyzers.Native.Index;
|
||||
using StellaOps.Scanner.Emit.Composition;
|
||||
using StellaOps.Scanner.Emit.Native;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Emit.Tests.Native;
|
||||
|
||||
public sealed class NativeBinarySbomIntegrationTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Compose_EmitsNativeBinariesAsFileComponents_WithBuildIdPurlAndLayerTracking()
|
||||
{
|
||||
var index = new FakeBuildIdIndex();
|
||||
index.AddEntry("gnu-build-id:abc123", new BuildIdLookupResult(
|
||||
BuildId: "gnu-build-id:abc123",
|
||||
Purl: "pkg:deb/debian/libc6@2.31",
|
||||
Version: "2.31",
|
||||
SourceDistro: "debian",
|
||||
Confidence: BuildIdConfidence.Exact,
|
||||
IndexedAt: new DateTimeOffset(2025, 12, 19, 0, 0, 0, TimeSpan.Zero)));
|
||||
|
||||
var emitter = new NativeComponentEmitter(index, NullLogger<NativeComponentEmitter>.Instance);
|
||||
var mapper = new NativeComponentMapper(emitter);
|
||||
|
||||
const string layer1 = "sha256:layer1";
|
||||
const string layer2 = "sha256:layer2";
|
||||
|
||||
var resolvedBinary = new NativeBinaryMetadata
|
||||
{
|
||||
Format = "elf",
|
||||
FilePath = "/usr/lib/libc.so.6",
|
||||
BuildId = "GNU-BUILD-ID:ABC123",
|
||||
Architecture = "x86_64",
|
||||
Platform = "linux",
|
||||
};
|
||||
|
||||
var unresolvedBinary = new NativeBinaryMetadata
|
||||
{
|
||||
Format = "elf",
|
||||
FilePath = "/usr/lib/libssl.so.3",
|
||||
BuildId = "gnu-build-id:def456",
|
||||
Architecture = "x86_64",
|
||||
Platform = "linux",
|
||||
};
|
||||
|
||||
var mappingLayer1 = await mapper.MapLayerAsync(layer1, new[] { resolvedBinary, unresolvedBinary });
|
||||
var mappingLayer2 = await mapper.MapLayerAsync(layer2, new[] { resolvedBinary });
|
||||
|
||||
var request = SbomCompositionRequest.Create(
|
||||
new ImageArtifactDescriptor
|
||||
{
|
||||
ImageDigest = "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
||||
ImageReference = "registry.example.com/app/service:1.2.3",
|
||||
Repository = "registry.example.com/app/service",
|
||||
Tag = "1.2.3",
|
||||
Architecture = "amd64",
|
||||
},
|
||||
new[] { mappingLayer1.ToFragment(), mappingLayer2.ToFragment() },
|
||||
new DateTimeOffset(2025, 12, 19, 12, 0, 0, TimeSpan.Zero),
|
||||
generatorName: "StellaOps.Scanner",
|
||||
generatorVersion: "0.10.0");
|
||||
|
||||
var composer = new CycloneDxComposer();
|
||||
var result = composer.Compose(request);
|
||||
|
||||
using var document = JsonDocument.Parse(result.Inventory.JsonBytes);
|
||||
var components = document.RootElement.GetProperty("components").EnumerateArray().ToArray();
|
||||
Assert.Equal(2, components.Length);
|
||||
|
||||
var resolvedPurl = mappingLayer1.Components.Single(component => component.IndexMatch).Purl;
|
||||
var unresolvedPurl = mappingLayer1.Components.Single(component => !component.IndexMatch).Purl;
|
||||
|
||||
var resolvedComponent = components.Single(component => string.Equals(component.GetProperty("purl").GetString(), resolvedPurl, StringComparison.Ordinal));
|
||||
Assert.Equal("file", resolvedComponent.GetProperty("type").GetString());
|
||||
Assert.Equal(resolvedPurl, resolvedComponent.GetProperty("bom-ref").GetString());
|
||||
|
||||
var resolvedProperties = resolvedComponent
|
||||
.GetProperty("properties")
|
||||
.EnumerateArray()
|
||||
.ToDictionary(
|
||||
property => property.GetProperty("name").GetString()!,
|
||||
property => property.GetProperty("value").GetString()!,
|
||||
StringComparer.Ordinal);
|
||||
|
||||
Assert.Equal("gnu-build-id:abc123", resolvedProperties["stellaops:buildId"]);
|
||||
Assert.Equal("elf", resolvedProperties["stellaops:binary.format"]);
|
||||
Assert.Equal(layer1, resolvedProperties["stellaops:firstLayerDigest"]);
|
||||
Assert.Equal(layer2, resolvedProperties["stellaops:lastLayerDigest"]);
|
||||
Assert.Equal($"{layer1},{layer2}", resolvedProperties["stellaops:layerDigests"]);
|
||||
|
||||
var unresolvedComponent = components.Single(component => string.Equals(component.GetProperty("purl").GetString(), unresolvedPurl, StringComparison.Ordinal));
|
||||
Assert.Equal("file", unresolvedComponent.GetProperty("type").GetString());
|
||||
Assert.StartsWith("pkg:generic/libssl.so.3@unknown", unresolvedPurl, StringComparison.Ordinal);
|
||||
Assert.Contains("build-id=gnu-build-id%3Adef456", unresolvedPurl, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private sealed class FakeBuildIdIndex : IBuildIdIndex
|
||||
{
|
||||
private readonly Dictionary<string, BuildIdLookupResult> _entries = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public int Count => _entries.Count;
|
||||
public bool IsLoaded => true;
|
||||
|
||||
public void AddEntry(string buildId, BuildIdLookupResult result)
|
||||
{
|
||||
_entries[buildId] = result;
|
||||
}
|
||||
|
||||
public Task<BuildIdLookupResult?> LookupAsync(string buildId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_entries.TryGetValue(buildId, out var result);
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<BuildIdLookupResult>> BatchLookupAsync(IEnumerable<string> buildIds, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var results = buildIds
|
||||
.Select(id => _entries.TryGetValue(id, out var result) ? result : null)
|
||||
.Where(result => result is not null)
|
||||
.Select(result => result!)
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult<IReadOnlyList<BuildIdLookupResult>>(results);
|
||||
}
|
||||
|
||||
public Task LoadAsync(CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user