up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
This commit is contained in:
@@ -1,26 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
using StellaOps.Scanner.Emit.Composition;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Emit.Tests.Composition;
|
||||
|
||||
public sealed class CycloneDxComposerTests
|
||||
{
|
||||
[Fact]
|
||||
public void Compose_ProducesInventoryAndUsageArtifacts()
|
||||
{
|
||||
var request = BuildRequest();
|
||||
var composer = new CycloneDxComposer();
|
||||
|
||||
var result = composer.Compose(request);
|
||||
|
||||
Assert.NotNull(result.Inventory);
|
||||
Assert.StartsWith("urn:uuid:", result.Inventory.SerialNumber, StringComparison.Ordinal);
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
using StellaOps.Scanner.Emit.Composition;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Emit.Tests.Composition;
|
||||
|
||||
public sealed class CycloneDxComposerTests
|
||||
{
|
||||
[Fact]
|
||||
public void Compose_ProducesInventoryAndUsageArtifacts()
|
||||
{
|
||||
var request = BuildRequest();
|
||||
var composer = new CycloneDxComposer();
|
||||
|
||||
var result = composer.Compose(request);
|
||||
|
||||
Assert.NotNull(result.Inventory);
|
||||
Assert.StartsWith("urn:uuid:", result.Inventory.SerialNumber, StringComparison.Ordinal);
|
||||
Assert.Equal("application/vnd.cyclonedx+json; version=1.6", result.Inventory.JsonMediaType);
|
||||
Assert.Equal("application/vnd.cyclonedx+protobuf; version=1.6", result.Inventory.ProtobufMediaType);
|
||||
Assert.Equal(2, result.Inventory.Components.Length);
|
||||
@@ -64,17 +64,17 @@ public sealed class CycloneDxComposerTests
|
||||
var usageVulns = usageVulnerabilities.EnumerateArray().ToArray();
|
||||
Assert.Single(usageVulns);
|
||||
Assert.Equal("finding-a", usageVulns[0].GetProperty("bom-ref").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compose_IsDeterministic()
|
||||
{
|
||||
var request = BuildRequest();
|
||||
var composer = new CycloneDxComposer();
|
||||
|
||||
var first = composer.Compose(request);
|
||||
var second = composer.Compose(request);
|
||||
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compose_IsDeterministic()
|
||||
{
|
||||
var request = BuildRequest();
|
||||
var composer = new CycloneDxComposer();
|
||||
|
||||
var first = composer.Compose(request);
|
||||
var second = composer.Compose(request);
|
||||
|
||||
Assert.Equal(first.Inventory.JsonSha256, second.Inventory.JsonSha256);
|
||||
Assert.Equal(first.Inventory.ContentHash, first.Inventory.JsonSha256);
|
||||
Assert.Equal(first.Inventory.ProtobufSha256, second.Inventory.ProtobufSha256);
|
||||
@@ -99,20 +99,20 @@ public sealed class CycloneDxComposerTests
|
||||
Assert.Equal(result.CompositionRecipeSha256.Length, 64);
|
||||
Assert.NotEmpty(result.CompositionRecipeJson);
|
||||
}
|
||||
|
||||
private static SbomCompositionRequest BuildRequest()
|
||||
{
|
||||
var fragments = new[]
|
||||
{
|
||||
LayerComponentFragment.Create("sha256:layer1", new[]
|
||||
{
|
||||
new ComponentRecord
|
||||
{
|
||||
Identity = ComponentIdentity.Create("pkg:npm/a", "component-a", "1.0.0", "pkg:npm/a@1.0.0", "library"),
|
||||
LayerDigest = "sha256:layer1",
|
||||
Evidence = ImmutableArray.Create(ComponentEvidence.FromPath("/app/node_modules/a/package.json")),
|
||||
Dependencies = ImmutableArray.Create("pkg:npm/b"),
|
||||
Usage = ComponentUsage.Create(true, new[] { "/app/start.sh" }),
|
||||
|
||||
private static SbomCompositionRequest BuildRequest()
|
||||
{
|
||||
var fragments = new[]
|
||||
{
|
||||
LayerComponentFragment.Create("sha256:layer1", new[]
|
||||
{
|
||||
new ComponentRecord
|
||||
{
|
||||
Identity = ComponentIdentity.Create("pkg:npm/a", "component-a", "1.0.0", "pkg:npm/a@1.0.0", "library"),
|
||||
LayerDigest = "sha256:layer1",
|
||||
Evidence = ImmutableArray.Create(ComponentEvidence.FromPath("/app/node_modules/a/package.json")),
|
||||
Dependencies = ImmutableArray.Create("pkg:npm/b"),
|
||||
Usage = ComponentUsage.Create(true, new[] { "/app/start.sh" }),
|
||||
Metadata = new ComponentMetadata
|
||||
{
|
||||
Scope = "runtime",
|
||||
@@ -127,35 +127,35 @@ public sealed class CycloneDxComposerTests
|
||||
},
|
||||
}
|
||||
}),
|
||||
LayerComponentFragment.Create("sha256:layer2", new[]
|
||||
{
|
||||
new ComponentRecord
|
||||
{
|
||||
Identity = ComponentIdentity.Create("pkg:npm/b", "component-b", "2.0.0", "pkg:npm/b@2.0.0", "library"),
|
||||
LayerDigest = "sha256:layer2",
|
||||
Evidence = ImmutableArray.Create(ComponentEvidence.FromPath("/app/node_modules/b/package.json")),
|
||||
Usage = ComponentUsage.Create(false),
|
||||
Metadata = new ComponentMetadata
|
||||
{
|
||||
Scope = "development",
|
||||
Properties = new Dictionary<string, string>
|
||||
{
|
||||
["stellaops.os.analyzer"] = "language-node",
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
var image = new ImageArtifactDescriptor
|
||||
{
|
||||
ImageDigest = "sha256:1234567890abcdef",
|
||||
ImageReference = "registry.example.com/app/service:1.2.3",
|
||||
Repository = "registry.example.com/app/service",
|
||||
Tag = "1.2.3",
|
||||
Architecture = "amd64",
|
||||
};
|
||||
|
||||
LayerComponentFragment.Create("sha256:layer2", new[]
|
||||
{
|
||||
new ComponentRecord
|
||||
{
|
||||
Identity = ComponentIdentity.Create("pkg:npm/b", "component-b", "2.0.0", "pkg:npm/b@2.0.0", "library"),
|
||||
LayerDigest = "sha256:layer2",
|
||||
Evidence = ImmutableArray.Create(ComponentEvidence.FromPath("/app/node_modules/b/package.json")),
|
||||
Usage = ComponentUsage.Create(false),
|
||||
Metadata = new ComponentMetadata
|
||||
{
|
||||
Scope = "development",
|
||||
Properties = new Dictionary<string, string>
|
||||
{
|
||||
["stellaops.os.analyzer"] = "language-node",
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
var image = new ImageArtifactDescriptor
|
||||
{
|
||||
ImageDigest = "sha256:1234567890abcdef",
|
||||
ImageReference = "registry.example.com/app/service:1.2.3",
|
||||
Repository = "registry.example.com/app/service",
|
||||
Tag = "1.2.3",
|
||||
Architecture = "amd64",
|
||||
};
|
||||
|
||||
return SbomCompositionRequest.Create(
|
||||
image,
|
||||
fragments,
|
||||
@@ -204,22 +204,22 @@ public sealed class CycloneDxComposerTests
|
||||
new KeyValuePair<string, double>("trustWeight", 0.85))
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void ValidateJson(byte[] data, int expectedComponentCount, string expectedView)
|
||||
{
|
||||
using var document = JsonDocument.Parse(data);
|
||||
var root = document.RootElement;
|
||||
|
||||
Assert.True(root.TryGetProperty("metadata", out var metadata), "metadata property missing");
|
||||
var properties = metadata.GetProperty("properties");
|
||||
}
|
||||
|
||||
private static void ValidateJson(byte[] data, int expectedComponentCount, string expectedView)
|
||||
{
|
||||
using var document = JsonDocument.Parse(data);
|
||||
var root = document.RootElement;
|
||||
|
||||
Assert.True(root.TryGetProperty("metadata", out var metadata), "metadata property missing");
|
||||
var properties = metadata.GetProperty("properties");
|
||||
var viewProperty = properties.EnumerateArray()
|
||||
.Single(prop => string.Equals(prop.GetProperty("name").GetString(), "stellaops:sbom.view", StringComparison.Ordinal));
|
||||
Assert.Equal(expectedView, viewProperty.GetProperty("value").GetString());
|
||||
|
||||
var components = root.GetProperty("components").EnumerateArray().ToArray();
|
||||
Assert.Equal(expectedComponentCount, components.Length);
|
||||
|
||||
Assert.Equal(expectedView, viewProperty.GetProperty("value").GetString());
|
||||
|
||||
var components = root.GetProperty("components").EnumerateArray().ToArray();
|
||||
Assert.Equal(expectedComponentCount, components.Length);
|
||||
|
||||
var names = components.Select(component => component.GetProperty("name").GetString()!).ToArray();
|
||||
Assert.Equal(names, names.OrderBy(n => n, StringComparer.Ordinal).ToArray());
|
||||
|
||||
|
||||
@@ -1,52 +1,52 @@
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
using StellaOps.Scanner.Emit.Composition;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Emit.Tests.Composition;
|
||||
|
||||
public class ScanAnalysisCompositionBuilderTests
|
||||
{
|
||||
[Fact]
|
||||
public void FromAnalysis_BuildsRequest_WhenFragmentsPresent()
|
||||
{
|
||||
var analysis = new ScanAnalysisStore();
|
||||
var fragment = LayerComponentFragment.Create(
|
||||
"sha256:layer",
|
||||
new[]
|
||||
{
|
||||
new ComponentRecord
|
||||
{
|
||||
Identity = ComponentIdentity.Create("pkg:test/a", "a", "1.0.0", "pkg:test/a@1.0.0", "library"),
|
||||
LayerDigest = "sha256:layer",
|
||||
Evidence = ImmutableArray<ComponentEvidence>.Empty,
|
||||
Dependencies = ImmutableArray<string>.Empty,
|
||||
Metadata = null,
|
||||
Usage = ComponentUsage.Unused,
|
||||
}
|
||||
});
|
||||
|
||||
analysis.AppendLayerFragments(new[] { fragment });
|
||||
|
||||
var request = ScanAnalysisCompositionBuilder.FromAnalysis(
|
||||
analysis,
|
||||
new ImageArtifactDescriptor { ImageDigest = "sha256:image" },
|
||||
DateTimeOffset.UtcNow,
|
||||
generatorName: "test",
|
||||
generatorVersion: "1.0.0");
|
||||
|
||||
Assert.Equal("sha256:image", request.Image.ImageDigest);
|
||||
Assert.Single(request.LayerFragments);
|
||||
Assert.Equal(fragment.LayerDigest, request.LayerFragments[0].LayerDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildComponentGraph_ReturnsEmpty_WhenNoFragments()
|
||||
{
|
||||
var analysis = new ScanAnalysisStore();
|
||||
var graph = ScanAnalysisCompositionBuilder.BuildComponentGraph(analysis);
|
||||
|
||||
Assert.Empty(graph.Components);
|
||||
Assert.Empty(graph.Layers);
|
||||
}
|
||||
}
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
using StellaOps.Scanner.Emit.Composition;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Emit.Tests.Composition;
|
||||
|
||||
public class ScanAnalysisCompositionBuilderTests
|
||||
{
|
||||
[Fact]
|
||||
public void FromAnalysis_BuildsRequest_WhenFragmentsPresent()
|
||||
{
|
||||
var analysis = new ScanAnalysisStore();
|
||||
var fragment = LayerComponentFragment.Create(
|
||||
"sha256:layer",
|
||||
new[]
|
||||
{
|
||||
new ComponentRecord
|
||||
{
|
||||
Identity = ComponentIdentity.Create("pkg:test/a", "a", "1.0.0", "pkg:test/a@1.0.0", "library"),
|
||||
LayerDigest = "sha256:layer",
|
||||
Evidence = ImmutableArray<ComponentEvidence>.Empty,
|
||||
Dependencies = ImmutableArray<string>.Empty,
|
||||
Metadata = null,
|
||||
Usage = ComponentUsage.Unused,
|
||||
}
|
||||
});
|
||||
|
||||
analysis.AppendLayerFragments(new[] { fragment });
|
||||
|
||||
var request = ScanAnalysisCompositionBuilder.FromAnalysis(
|
||||
analysis,
|
||||
new ImageArtifactDescriptor { ImageDigest = "sha256:image" },
|
||||
DateTimeOffset.UtcNow,
|
||||
generatorName: "test",
|
||||
generatorVersion: "1.0.0");
|
||||
|
||||
Assert.Equal("sha256:image", request.Image.ImageDigest);
|
||||
Assert.Single(request.LayerFragments);
|
||||
Assert.Equal(fragment.LayerDigest, request.LayerFragments[0].LayerDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildComponentGraph_ReturnsEmpty_WhenNoFragments()
|
||||
{
|
||||
var analysis = new ScanAnalysisStore();
|
||||
var graph = ScanAnalysisCompositionBuilder.BuildComponentGraph(analysis);
|
||||
|
||||
Assert.Empty(graph.Components);
|
||||
Assert.Empty(graph.Layers);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,141 +1,141 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,69 +1,69 @@
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
using StellaOps.Scanner.Emit.Composition;
|
||||
using StellaOps.Scanner.Emit.Index;
|
||||
using StellaOps.Scanner.Emit.Packaging;
|
||||
|
||||
namespace StellaOps.Scanner.Emit.Tests.Packaging;
|
||||
|
||||
public sealed class ScannerArtifactPackageBuilderTests
|
||||
{
|
||||
[Fact]
|
||||
public void BuildPackage_ProducesDescriptorsAndManifest()
|
||||
{
|
||||
var fragments = new[]
|
||||
{
|
||||
LayerComponentFragment.Create("sha256:layer1", new[]
|
||||
{
|
||||
CreateComponent(
|
||||
"pkg:npm/a",
|
||||
"1.0.0",
|
||||
"sha256:layer1",
|
||||
usage: ComponentUsage.Create(true, new[] { "/app/start.sh" }),
|
||||
metadata: new Dictionary<string, string>
|
||||
{
|
||||
["stellaops.os.analyzer"] = "apk",
|
||||
["stellaops.os.architecture"] = "x86_64",
|
||||
}),
|
||||
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.0.0", "sha256:layer2", usage: ComponentUsage.Create(true, new[] { "/app/init.sh" })),
|
||||
})
|
||||
};
|
||||
|
||||
var request = SbomCompositionRequest.Create(
|
||||
new ImageArtifactDescriptor
|
||||
{
|
||||
ImageDigest = "sha256:image",
|
||||
ImageReference = "registry.example/app:latest",
|
||||
Repository = "registry.example/app",
|
||||
Tag = "latest",
|
||||
},
|
||||
fragments,
|
||||
new DateTimeOffset(2025, 10, 19, 12, 30, 0, TimeSpan.Zero),
|
||||
generatorName: "StellaOps.Scanner",
|
||||
generatorVersion: "0.10.0");
|
||||
|
||||
var composer = new CycloneDxComposer();
|
||||
var composition = composer.Compose(request);
|
||||
|
||||
var indexBuilder = new BomIndexBuilder();
|
||||
var bomIndex = indexBuilder.Build(new BomIndexBuildRequest
|
||||
{
|
||||
ImageDigest = request.Image.ImageDigest,
|
||||
Graph = composition.Graph,
|
||||
GeneratedAt = request.GeneratedAt,
|
||||
});
|
||||
|
||||
var packageBuilder = new ScannerArtifactPackageBuilder();
|
||||
var package = packageBuilder.Build(request.Image.ImageDigest, request.GeneratedAt, composition, bomIndex);
|
||||
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
using StellaOps.Scanner.Emit.Composition;
|
||||
using StellaOps.Scanner.Emit.Index;
|
||||
using StellaOps.Scanner.Emit.Packaging;
|
||||
|
||||
namespace StellaOps.Scanner.Emit.Tests.Packaging;
|
||||
|
||||
public sealed class ScannerArtifactPackageBuilderTests
|
||||
{
|
||||
[Fact]
|
||||
public void BuildPackage_ProducesDescriptorsAndManifest()
|
||||
{
|
||||
var fragments = new[]
|
||||
{
|
||||
LayerComponentFragment.Create("sha256:layer1", new[]
|
||||
{
|
||||
CreateComponent(
|
||||
"pkg:npm/a",
|
||||
"1.0.0",
|
||||
"sha256:layer1",
|
||||
usage: ComponentUsage.Create(true, new[] { "/app/start.sh" }),
|
||||
metadata: new Dictionary<string, string>
|
||||
{
|
||||
["stellaops.os.analyzer"] = "apk",
|
||||
["stellaops.os.architecture"] = "x86_64",
|
||||
}),
|
||||
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.0.0", "sha256:layer2", usage: ComponentUsage.Create(true, new[] { "/app/init.sh" })),
|
||||
})
|
||||
};
|
||||
|
||||
var request = SbomCompositionRequest.Create(
|
||||
new ImageArtifactDescriptor
|
||||
{
|
||||
ImageDigest = "sha256:image",
|
||||
ImageReference = "registry.example/app:latest",
|
||||
Repository = "registry.example/app",
|
||||
Tag = "latest",
|
||||
},
|
||||
fragments,
|
||||
new DateTimeOffset(2025, 10, 19, 12, 30, 0, TimeSpan.Zero),
|
||||
generatorName: "StellaOps.Scanner",
|
||||
generatorVersion: "0.10.0");
|
||||
|
||||
var composer = new CycloneDxComposer();
|
||||
var composition = composer.Compose(request);
|
||||
|
||||
var indexBuilder = new BomIndexBuilder();
|
||||
var bomIndex = indexBuilder.Build(new BomIndexBuildRequest
|
||||
{
|
||||
ImageDigest = request.Image.ImageDigest,
|
||||
Graph = composition.Graph,
|
||||
GeneratedAt = request.GeneratedAt,
|
||||
});
|
||||
|
||||
var packageBuilder = new ScannerArtifactPackageBuilder();
|
||||
var package = packageBuilder.Build(request.Image.ImageDigest, request.GeneratedAt, composition, bomIndex);
|
||||
|
||||
Assert.Equal(6, package.Artifacts.Length); // inventory JSON+PB, usage JSON+PB, index, composition recipe
|
||||
|
||||
var kinds = package.Manifest.Artifacts.Select(entry => entry.Kind).ToArray();
|
||||
@@ -74,24 +74,24 @@ public sealed class ScannerArtifactPackageBuilderTests
|
||||
var root = document.RootElement;
|
||||
Assert.Equal("sha256:image", root.GetProperty("imageDigest").GetString());
|
||||
Assert.Equal(6, root.GetProperty("artifacts").GetArrayLength());
|
||||
|
||||
var usageEntry = root.GetProperty("artifacts").EnumerateArray().First(element => element.GetProperty("kind").GetString() == "sbom-usage");
|
||||
|
||||
var usageEntry = root.GetProperty("artifacts").EnumerateArray().First(element => element.GetProperty("kind").GetString() == "sbom-usage");
|
||||
Assert.Equal("application/vnd.cyclonedx+json; version=1.6; view=usage", usageEntry.GetProperty("mediaType").GetString());
|
||||
}
|
||||
|
||||
private static ComponentRecord CreateComponent(string key, string version, string layerDigest, ComponentUsage? usage = null, IReadOnlyDictionary<string, string>? metadata = null)
|
||||
{
|
||||
return new ComponentRecord
|
||||
{
|
||||
Identity = ComponentIdentity.Create(key, key.Split('/', 2)[^1], version, key, "library"),
|
||||
LayerDigest = layerDigest,
|
||||
Usage = usage ?? ComponentUsage.Unused,
|
||||
Metadata = metadata is null
|
||||
? null
|
||||
: new ComponentMetadata
|
||||
{
|
||||
Properties = metadata,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ComponentRecord CreateComponent(string key, string version, string layerDigest, ComponentUsage? usage = null, IReadOnlyDictionary<string, string>? metadata = null)
|
||||
{
|
||||
return new ComponentRecord
|
||||
{
|
||||
Identity = ComponentIdentity.Create(key, key.Split('/', 2)[^1], version, key, "library"),
|
||||
LayerDigest = layerDigest,
|
||||
Usage = usage ?? ComponentUsage.Unused,
|
||||
Metadata = metadata is null
|
||||
? null
|
||||
: new ComponentMetadata
|
||||
{
|
||||
Properties = metadata,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user