142 lines
5.2 KiB
C#
142 lines
5.2 KiB
C#
using System;
|
|
using System.Collections.Immutable;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using Collections.Special;
|
|
using StellaOps.Scanner.Core.Contracts;
|
|
using StellaOps.Scanner.Emit.Index;
|
|
|
|
namespace StellaOps.Scanner.Emit.Tests.Index;
|
|
|
|
public sealed class BomIndexBuilderTests
|
|
{
|
|
[Fact]
|
|
public void Build_GeneratesDeterministicBinaryIndex_WithUsageBitmaps()
|
|
{
|
|
var graph = ComponentGraphBuilder.Build(new[]
|
|
{
|
|
LayerComponentFragment.Create("sha256:layer1", new[]
|
|
{
|
|
CreateComponent("pkg:npm/a", "1.0.0", "sha256:layer1", usageEntrypoints: new[] { "/app/start.sh" }),
|
|
CreateComponent("pkg:npm/b", "2.0.0", "sha256:layer1"),
|
|
}),
|
|
LayerComponentFragment.Create("sha256:layer2", new[]
|
|
{
|
|
CreateComponent("pkg:npm/b", "2.0.0", "sha256:layer2"),
|
|
CreateComponent("pkg:npm/c", "3.1.0", "sha256:layer2", usageEntrypoints: new[] { "/app/init.sh" }),
|
|
}),
|
|
});
|
|
|
|
var request = new BomIndexBuildRequest
|
|
{
|
|
ImageDigest = "sha256:image",
|
|
Graph = graph,
|
|
GeneratedAt = new DateTimeOffset(2025, 10, 19, 9, 45, 0, TimeSpan.Zero),
|
|
};
|
|
|
|
var builder = new BomIndexBuilder();
|
|
var artifact = builder.Build(request);
|
|
var second = builder.Build(request);
|
|
|
|
Assert.Equal(artifact.Sha256, second.Sha256);
|
|
Assert.Equal(artifact.Bytes, second.Bytes);
|
|
Assert.Equal(2, artifact.LayerCount);
|
|
Assert.Equal(3, artifact.ComponentCount);
|
|
Assert.Equal(2, artifact.EntrypointCount);
|
|
|
|
using var reader = new BinaryReader(new MemoryStream(artifact.Bytes), System.Text.Encoding.UTF8, leaveOpen: false);
|
|
ValidateHeader(reader, request);
|
|
var layers = ReadTable(reader, artifact.LayerCount);
|
|
Assert.Equal(new[] { "sha256:layer1", "sha256:layer2" }, layers);
|
|
|
|
var purls = ReadTable(reader, artifact.ComponentCount);
|
|
Assert.Equal(new[] { "pkg:npm/a", "pkg:npm/b", "pkg:npm/c" }, purls);
|
|
|
|
var componentBitmaps = ReadBitmaps(reader, artifact.ComponentCount);
|
|
Assert.Equal(new[] { new[] { 0 }, new[] { 0, 1 }, new[] { 1 } }, componentBitmaps);
|
|
|
|
var entrypoints = ReadTable(reader, artifact.EntrypointCount);
|
|
Assert.Equal(new[] { "/app/init.sh", "/app/start.sh" }, entrypoints);
|
|
|
|
var usageBitmaps = ReadBitmaps(reader, artifact.ComponentCount);
|
|
Assert.Equal(new[] { new[] { 1 }, Array.Empty<int>(), new[] { 0 } }, usageBitmaps);
|
|
}
|
|
|
|
private static void ValidateHeader(BinaryReader reader, BomIndexBuildRequest request)
|
|
{
|
|
var magic = reader.ReadBytes(7);
|
|
Assert.Equal("BOMIDX1", System.Text.Encoding.ASCII.GetString(magic));
|
|
|
|
var version = reader.ReadUInt16();
|
|
Assert.Equal(1u, version);
|
|
|
|
var flags = reader.ReadUInt16();
|
|
Assert.Equal(0x1, flags);
|
|
|
|
var digestLength = reader.ReadUInt16();
|
|
var digestBytes = reader.ReadBytes(digestLength);
|
|
Assert.Equal(request.ImageDigest, System.Text.Encoding.UTF8.GetString(digestBytes));
|
|
|
|
var unixMicroseconds = reader.ReadInt64();
|
|
var expectedMicroseconds = request.GeneratedAt.ToUniversalTime().ToUnixTimeMilliseconds() * 1000L;
|
|
expectedMicroseconds += request.GeneratedAt.ToUniversalTime().Ticks % TimeSpan.TicksPerMillisecond / 10;
|
|
Assert.Equal(expectedMicroseconds, unixMicroseconds);
|
|
|
|
var layers = reader.ReadUInt32();
|
|
var components = reader.ReadUInt32();
|
|
var entrypoints = reader.ReadUInt32();
|
|
|
|
Assert.Equal(2u, layers);
|
|
Assert.Equal(3u, components);
|
|
Assert.Equal(2u, entrypoints);
|
|
}
|
|
|
|
private static string[] ReadTable(BinaryReader reader, int count)
|
|
{
|
|
var values = new string[count];
|
|
for (var i = 0; i < count; i++)
|
|
{
|
|
var length = reader.ReadUInt16();
|
|
var bytes = reader.ReadBytes(length);
|
|
values[i] = System.Text.Encoding.UTF8.GetString(bytes);
|
|
}
|
|
|
|
return values;
|
|
}
|
|
|
|
private static int[][] ReadBitmaps(BinaryReader reader, int count)
|
|
{
|
|
var result = new int[count][];
|
|
for (var i = 0; i < count; i++)
|
|
{
|
|
var length = reader.ReadUInt32();
|
|
if (length == 0)
|
|
{
|
|
result[i] = Array.Empty<int>();
|
|
continue;
|
|
}
|
|
|
|
var bytes = reader.ReadBytes((int)length);
|
|
using var ms = new MemoryStream(bytes, writable: false);
|
|
var bitmap = RoaringBitmap.Deserialize(ms);
|
|
result[i] = bitmap.ToArray();
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static ComponentRecord CreateComponent(string key, string version, string layerDigest, string[]? usageEntrypoints = null)
|
|
{
|
|
var usage = usageEntrypoints is null
|
|
? ComponentUsage.Unused
|
|
: ComponentUsage.Create(true, usageEntrypoints);
|
|
|
|
return new ComponentRecord
|
|
{
|
|
Identity = ComponentIdentity.Create(key, key.Split('/', 2)[^1], version, key, "library"),
|
|
LayerDigest = layerDigest,
|
|
Usage = usage,
|
|
};
|
|
}
|
|
}
|