Restructure solution layout by module
This commit is contained in:
@@ -0,0 +1,367 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Globalization;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
using StellaOps.Scanner.Diff;
|
||||
using StellaOps.Scanner.Core.Utility;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Diff.Tests;
|
||||
|
||||
public sealed class ComponentDifferTests
|
||||
{
|
||||
[Fact]
|
||||
public void Compute_CapturesAddedRemovedAndChangedComponents()
|
||||
{
|
||||
var oldFragments = new[]
|
||||
{
|
||||
LayerComponentFragment.Create("sha256:layer1", new[]
|
||||
{
|
||||
CreateComponent(
|
||||
"pkg:npm/a",
|
||||
version: "1.0.0",
|
||||
layer: "sha256:layer1",
|
||||
usage: ComponentUsage.Create(true, new[] { "/app/start.sh" }),
|
||||
evidence: new[] { ComponentEvidence.FromPath("/app/package-lock.json") }),
|
||||
CreateComponent("pkg:npm/b", version: "2.0.0", layer: "sha256:layer1", scope: "runtime"),
|
||||
}),
|
||||
LayerComponentFragment.Create("sha256:layer1b", new[]
|
||||
{
|
||||
CreateComponent(
|
||||
"pkg:npm/a",
|
||||
version: "1.0.0",
|
||||
layer: "sha256:layer1b",
|
||||
usage: ComponentUsage.Create(true, new[] { "/app/start.sh" })),
|
||||
CreateComponent("pkg:npm/d", version: "0.9.0", layer: "sha256:layer1b"),
|
||||
})
|
||||
};
|
||||
|
||||
var newFragments = new[]
|
||||
{
|
||||
LayerComponentFragment.Create("sha256:layer2", new[]
|
||||
{
|
||||
CreateComponent(
|
||||
"pkg:npm/a",
|
||||
version: "1.1.0",
|
||||
layer: "sha256:layer2",
|
||||
usage: ComponentUsage.Create(true, new[] { "/app/start.sh" }),
|
||||
evidence: new[] { ComponentEvidence.FromPath("/app/package-lock.json") }),
|
||||
}),
|
||||
LayerComponentFragment.Create("sha256:layer3", new[]
|
||||
{
|
||||
CreateComponent(
|
||||
"pkg:npm/b",
|
||||
version: "2.0.0",
|
||||
layer: "sha256:layer3",
|
||||
usage: ComponentUsage.Create(true, new[] { "/app/init.sh" }),
|
||||
scope: "runtime"),
|
||||
CreateComponent("pkg:npm/c", version: "3.0.0", layer: "sha256:layer3"),
|
||||
})
|
||||
};
|
||||
|
||||
var oldGraph = ComponentGraphBuilder.Build(oldFragments);
|
||||
var newGraph = ComponentGraphBuilder.Build(newFragments);
|
||||
|
||||
var request = new ComponentDiffRequest
|
||||
{
|
||||
OldGraph = oldGraph,
|
||||
NewGraph = newGraph,
|
||||
GeneratedAt = new DateTimeOffset(2025, 10, 19, 10, 0, 0, TimeSpan.Zero),
|
||||
View = SbomView.Inventory,
|
||||
OldImageDigest = "sha256:old",
|
||||
NewImageDigest = "sha256:new",
|
||||
};
|
||||
|
||||
var differ = new ComponentDiffer();
|
||||
var document = differ.Compute(request);
|
||||
|
||||
Assert.Equal(SbomView.Inventory, document.View);
|
||||
Assert.Equal("sha256:old", document.OldImageDigest);
|
||||
Assert.Equal("sha256:new", document.NewImageDigest);
|
||||
Assert.Equal(1, document.Summary.Added);
|
||||
Assert.Equal(1, document.Summary.Removed);
|
||||
Assert.Equal(1, document.Summary.VersionChanged);
|
||||
Assert.Equal(1, document.Summary.MetadataChanged);
|
||||
|
||||
Assert.Equal(new[] { "sha256:layer2", "sha256:layer3", "sha256:layer1b" }, document.Layers.Select(layer => layer.LayerDigest));
|
||||
|
||||
var layerGroups = document.Layers.ToDictionary(layer => layer.LayerDigest);
|
||||
Assert.True(layerGroups.ContainsKey("sha256:layer2"), "Expected layer2 group present");
|
||||
Assert.True(layerGroups.ContainsKey("sha256:layer3"), "Expected layer3 group present");
|
||||
Assert.True(layerGroups.ContainsKey("sha256:layer1b"), "Expected layer1b group present");
|
||||
|
||||
var addedChange = layerGroups["sha256:layer3"].Changes.Single(change => change.Kind == ComponentChangeKind.Added);
|
||||
Assert.Equal("pkg:npm/c", addedChange.ComponentKey);
|
||||
Assert.NotNull(addedChange.NewComponent);
|
||||
|
||||
var versionChange = layerGroups["sha256:layer2"].Changes.Single(change => change.Kind == ComponentChangeKind.VersionChanged);
|
||||
Assert.Equal("pkg:npm/a", versionChange.ComponentKey);
|
||||
Assert.Equal("sha256:layer1b", versionChange.RemovingLayer);
|
||||
Assert.Equal("sha256:layer2", versionChange.IntroducingLayer);
|
||||
Assert.Equal("1.1.0", versionChange.NewComponent!.Identity.Version);
|
||||
|
||||
var metadataChange = layerGroups["sha256:layer3"].Changes.Single(change => change.Kind == ComponentChangeKind.MetadataChanged);
|
||||
Assert.True(metadataChange.NewComponent!.Usage.UsedByEntrypoint);
|
||||
Assert.False(metadataChange.OldComponent!.Usage.UsedByEntrypoint);
|
||||
Assert.Equal("sha256:layer3", metadataChange.IntroducingLayer);
|
||||
Assert.Equal("sha256:layer1", metadataChange.RemovingLayer);
|
||||
|
||||
var removedChange = layerGroups["sha256:layer1b"].Changes.Single(change => change.Kind == ComponentChangeKind.Removed);
|
||||
Assert.Equal("pkg:npm/d", removedChange.ComponentKey);
|
||||
Assert.Equal("sha256:layer1b", removedChange.RemovingLayer);
|
||||
Assert.Null(removedChange.IntroducingLayer);
|
||||
|
||||
var json = DiffJsonSerializer.Serialize(document);
|
||||
using var parsed = JsonDocument.Parse(json);
|
||||
var root = parsed.RootElement;
|
||||
Assert.Equal("inventory", root.GetProperty("view").GetString());
|
||||
var generatedAt = DateTimeOffset.Parse(root.GetProperty("generatedAt").GetString()!, CultureInfo.InvariantCulture);
|
||||
Assert.Equal(request.GeneratedAt, generatedAt);
|
||||
Assert.Equal("sha256:old", root.GetProperty("oldImageDigest").GetString());
|
||||
Assert.Equal("sha256:new", root.GetProperty("newImageDigest").GetString());
|
||||
|
||||
var summaryJson = root.GetProperty("summary");
|
||||
Assert.Equal(1, summaryJson.GetProperty("added").GetInt32());
|
||||
Assert.Equal(1, summaryJson.GetProperty("removed").GetInt32());
|
||||
Assert.Equal(1, summaryJson.GetProperty("versionChanged").GetInt32());
|
||||
Assert.Equal(1, summaryJson.GetProperty("metadataChanged").GetInt32());
|
||||
|
||||
var layersJson = root.GetProperty("layers");
|
||||
Assert.Equal(3, layersJson.GetArrayLength());
|
||||
|
||||
var layer2Json = layersJson[0];
|
||||
Assert.Equal("sha256:layer2", layer2Json.GetProperty("layerDigest").GetString());
|
||||
var layer2Changes = layer2Json.GetProperty("changes");
|
||||
Assert.Equal(1, layer2Changes.GetArrayLength());
|
||||
var versionChangeJson = layer2Changes.EnumerateArray().Single();
|
||||
Assert.Equal("versionChanged", versionChangeJson.GetProperty("kind").GetString());
|
||||
Assert.Equal("pkg:npm/a", versionChangeJson.GetProperty("componentKey").GetString());
|
||||
Assert.Equal("sha256:layer2", versionChangeJson.GetProperty("introducingLayer").GetString());
|
||||
Assert.Equal("sha256:layer1b", versionChangeJson.GetProperty("removingLayer").GetString());
|
||||
Assert.Equal("1.1.0", versionChangeJson.GetProperty("newComponent").GetProperty("identity").GetProperty("version").GetString());
|
||||
|
||||
var layer3Json = layersJson[1];
|
||||
Assert.Equal("sha256:layer3", layer3Json.GetProperty("layerDigest").GetString());
|
||||
var layer3Changes = layer3Json.GetProperty("changes");
|
||||
Assert.Equal(2, layer3Changes.GetArrayLength());
|
||||
var layer3ChangeArray = layer3Changes.EnumerateArray().ToArray();
|
||||
var metadataChangeJson = layer3ChangeArray[0];
|
||||
Assert.Equal("metadataChanged", metadataChangeJson.GetProperty("kind").GetString());
|
||||
Assert.Equal("pkg:npm/b", metadataChangeJson.GetProperty("componentKey").GetString());
|
||||
Assert.Equal("sha256:layer3", metadataChangeJson.GetProperty("introducingLayer").GetString());
|
||||
Assert.Equal("sha256:layer1", metadataChangeJson.GetProperty("removingLayer").GetString());
|
||||
Assert.True(metadataChangeJson.GetProperty("newComponent").GetProperty("usage").GetProperty("usedByEntrypoint").GetBoolean());
|
||||
Assert.False(metadataChangeJson.GetProperty("oldComponent").GetProperty("usage").GetProperty("usedByEntrypoint").GetBoolean());
|
||||
|
||||
var addedJson = layer3ChangeArray[1];
|
||||
Assert.Equal("added", addedJson.GetProperty("kind").GetString());
|
||||
Assert.Equal("pkg:npm/c", addedJson.GetProperty("componentKey").GetString());
|
||||
Assert.Equal("sha256:layer3", addedJson.GetProperty("introducingLayer").GetString());
|
||||
Assert.False(addedJson.TryGetProperty("removingLayer", out _));
|
||||
|
||||
var removedLayerJson = layersJson[2];
|
||||
Assert.Equal("sha256:layer1b", removedLayerJson.GetProperty("layerDigest").GetString());
|
||||
var removedChanges = removedLayerJson.GetProperty("changes");
|
||||
Assert.Equal(1, removedChanges.GetArrayLength());
|
||||
var removedJson = removedChanges.EnumerateArray().Single();
|
||||
Assert.Equal("removed", removedJson.GetProperty("kind").GetString());
|
||||
Assert.Equal("pkg:npm/d", removedJson.GetProperty("componentKey").GetString());
|
||||
Assert.Equal("sha256:layer1b", removedJson.GetProperty("removingLayer").GetString());
|
||||
Assert.False(removedJson.TryGetProperty("introducingLayer", out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compute_UsageView_FiltersComponents()
|
||||
{
|
||||
var oldFragments = new[]
|
||||
{
|
||||
LayerComponentFragment.Create("sha256:base", new[]
|
||||
{
|
||||
CreateComponent("pkg:npm/a", "1", "sha256:base", usage: ComponentUsage.Create(false)),
|
||||
})
|
||||
};
|
||||
|
||||
var newFragments = new[]
|
||||
{
|
||||
LayerComponentFragment.Create("sha256:new", new[]
|
||||
{
|
||||
CreateComponent("pkg:npm/a", "1", "sha256:new", usage: ComponentUsage.Create(false)),
|
||||
CreateComponent("pkg:npm/b", "1", "sha256:new", usage: ComponentUsage.Create(true, new[] { "/entry" })),
|
||||
})
|
||||
};
|
||||
|
||||
var request = new ComponentDiffRequest
|
||||
{
|
||||
OldGraph = ComponentGraphBuilder.Build(oldFragments),
|
||||
NewGraph = ComponentGraphBuilder.Build(newFragments),
|
||||
View = SbomView.Usage,
|
||||
GeneratedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
|
||||
var differ = new ComponentDiffer();
|
||||
var document = differ.Compute(request);
|
||||
|
||||
Assert.Single(document.Layers);
|
||||
var layer = document.Layers[0];
|
||||
Assert.Single(layer.Changes);
|
||||
Assert.Equal(ComponentChangeKind.Added, layer.Changes[0].Kind);
|
||||
Assert.Equal("pkg:npm/b", layer.Changes[0].ComponentKey);
|
||||
|
||||
var json = DiffJsonSerializer.Serialize(document);
|
||||
using var parsed = JsonDocument.Parse(json);
|
||||
Assert.Equal("usage", parsed.RootElement.GetProperty("view").GetString());
|
||||
Assert.Equal(1, parsed.RootElement.GetProperty("summary").GetProperty("added").GetInt32());
|
||||
Assert.False(parsed.RootElement.TryGetProperty("oldImageDigest", out _));
|
||||
Assert.False(parsed.RootElement.TryGetProperty("newImageDigest", out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compute_MetadataChange_WhenEvidenceDiffers()
|
||||
{
|
||||
var oldFragments = new[]
|
||||
{
|
||||
LayerComponentFragment.Create("sha256:underlay", new[]
|
||||
{
|
||||
CreateComponent(
|
||||
"pkg:npm/a",
|
||||
version: "1.0.0",
|
||||
layer: "sha256:underlay",
|
||||
usage: ComponentUsage.Create(false),
|
||||
evidence: new[] { ComponentEvidence.FromPath("/workspace/package-lock.json") }),
|
||||
}),
|
||||
};
|
||||
|
||||
var newFragments = new[]
|
||||
{
|
||||
LayerComponentFragment.Create("sha256:overlay", new[]
|
||||
{
|
||||
CreateComponent(
|
||||
"pkg:npm/a",
|
||||
version: "1.0.0",
|
||||
layer: "sha256:overlay",
|
||||
usage: ComponentUsage.Create(false),
|
||||
evidence: new[]
|
||||
{
|
||||
ComponentEvidence.FromPath("/workspace/package-lock.json"),
|
||||
ComponentEvidence.FromPath("/workspace/yarn.lock"),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
var request = new ComponentDiffRequest
|
||||
{
|
||||
OldGraph = ComponentGraphBuilder.Build(oldFragments),
|
||||
NewGraph = ComponentGraphBuilder.Build(newFragments),
|
||||
GeneratedAt = new DateTimeOffset(2025, 10, 19, 12, 0, 0, TimeSpan.Zero),
|
||||
};
|
||||
|
||||
var differ = new ComponentDiffer();
|
||||
var document = differ.Compute(request);
|
||||
|
||||
Assert.Equal(0, document.Summary.Added);
|
||||
Assert.Equal(0, document.Summary.Removed);
|
||||
Assert.Equal(0, document.Summary.VersionChanged);
|
||||
Assert.Equal(1, document.Summary.MetadataChanged);
|
||||
|
||||
var layer = Assert.Single(document.Layers);
|
||||
Assert.Equal("sha256:overlay", layer.LayerDigest);
|
||||
|
||||
var change = Assert.Single(layer.Changes);
|
||||
Assert.Equal(ComponentChangeKind.MetadataChanged, change.Kind);
|
||||
Assert.Equal("sha256:overlay", change.IntroducingLayer);
|
||||
Assert.Equal("sha256:underlay", change.RemovingLayer);
|
||||
Assert.Equal(2, change.NewComponent!.Evidence.Length);
|
||||
Assert.Equal(1, change.OldComponent!.Evidence.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compute_MetadataChange_WhenBuildIdDiffers()
|
||||
{
|
||||
var oldFragments = new[]
|
||||
{
|
||||
LayerComponentFragment.Create("sha256:base", new[]
|
||||
{
|
||||
CreateComponent(
|
||||
"pkg:npm/a",
|
||||
version: "1.0.0",
|
||||
layer: "sha256:base",
|
||||
scope: "runtime",
|
||||
buildId: "ABCDEF1234567890ABCDEF1234567890ABCDEF12"),
|
||||
}),
|
||||
};
|
||||
|
||||
var newFragments = new[]
|
||||
{
|
||||
LayerComponentFragment.Create("sha256:overlay", new[]
|
||||
{
|
||||
CreateComponent(
|
||||
"pkg:npm/a",
|
||||
version: "1.0.0",
|
||||
layer: "sha256:overlay",
|
||||
scope: "runtime",
|
||||
buildId: "6e0d8f6aa1b2c3d4e5f60718293a4b5c6d7e8f90"),
|
||||
}),
|
||||
};
|
||||
|
||||
var request = new ComponentDiffRequest
|
||||
{
|
||||
OldGraph = ComponentGraphBuilder.Build(oldFragments),
|
||||
NewGraph = ComponentGraphBuilder.Build(newFragments),
|
||||
GeneratedAt = new DateTimeOffset(2025, 10, 19, 13, 0, 0, TimeSpan.Zero),
|
||||
};
|
||||
|
||||
var differ = new ComponentDiffer();
|
||||
var document = differ.Compute(request);
|
||||
|
||||
Assert.Equal(0, document.Summary.Added);
|
||||
Assert.Equal(0, document.Summary.Removed);
|
||||
Assert.Equal(0, document.Summary.VersionChanged);
|
||||
Assert.Equal(1, document.Summary.MetadataChanged);
|
||||
|
||||
var layer = Assert.Single(document.Layers);
|
||||
Assert.Equal("sha256:overlay", layer.LayerDigest);
|
||||
|
||||
var change = Assert.Single(layer.Changes);
|
||||
Assert.Equal(ComponentChangeKind.MetadataChanged, change.Kind);
|
||||
Assert.Equal("sha256:overlay", change.IntroducingLayer);
|
||||
Assert.Equal("sha256:base", change.RemovingLayer);
|
||||
Assert.Equal("abcdef1234567890abcdef1234567890abcdef12", change.OldComponent!.Metadata!.BuildId);
|
||||
Assert.Equal("6e0d8f6aa1b2c3d4e5f60718293a4b5c6d7e8f90", change.NewComponent!.Metadata!.BuildId);
|
||||
|
||||
var json = DiffJsonSerializer.Serialize(document);
|
||||
using var parsed = JsonDocument.Parse(json);
|
||||
var changeJson = parsed.RootElement
|
||||
.GetProperty("layers")[0]
|
||||
.GetProperty("changes")[0];
|
||||
Assert.Equal("abcdef1234567890abcdef1234567890abcdef12", changeJson.GetProperty("oldComponent").GetProperty("metadata").GetProperty("buildId").GetString());
|
||||
Assert.Equal("6e0d8f6aa1b2c3d4e5f60718293a4b5c6d7e8f90", changeJson.GetProperty("newComponent").GetProperty("metadata").GetProperty("buildId").GetString());
|
||||
}
|
||||
|
||||
private static ComponentRecord CreateComponent(
|
||||
string key,
|
||||
string version,
|
||||
string layer,
|
||||
ComponentUsage? usage = null,
|
||||
string? scope = null,
|
||||
IEnumerable<ComponentEvidence>? evidence = null,
|
||||
string? buildId = null)
|
||||
{
|
||||
return new ComponentRecord
|
||||
{
|
||||
Identity = ComponentIdentity.Create(key, key.Split('/', 2)[^1], version, purl: key, componentType: "library"),
|
||||
LayerDigest = layer,
|
||||
Usage = usage ?? ComponentUsage.Unused,
|
||||
Metadata = scope is null && buildId is null
|
||||
? null
|
||||
: new ComponentMetadata
|
||||
{
|
||||
Scope = scope,
|
||||
BuildId = buildId,
|
||||
},
|
||||
Evidence = evidence is null ? ImmutableArray<ComponentEvidence>.Empty : ImmutableArray.CreateRange(evidence),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Diff/StellaOps.Scanner.Diff.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user