Restructure solution layout by module
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
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);
|
||||
|
||||
Assert.NotNull(result.Usage);
|
||||
Assert.Equal("application/vnd.cyclonedx+json; version=1.6; view=usage", result.Usage!.JsonMediaType);
|
||||
Assert.Single(result.Usage.Components);
|
||||
Assert.Equal("pkg:npm/a", result.Usage.Components[0].Identity.Key);
|
||||
|
||||
ValidateJson(result.Inventory.JsonBytes, expectedComponentCount: 2, expectedView: "inventory");
|
||||
ValidateJson(result.Usage.JsonBytes, expectedComponentCount: 1, expectedView: "usage");
|
||||
|
||||
var inventoryComponentA = result.Inventory.Components.Single(component => string.Equals(component.Identity.Key, "pkg:npm/a", StringComparison.Ordinal));
|
||||
Assert.Equal("abcdef1234567890abcdef1234567890abcdef12", inventoryComponentA.Metadata?.BuildId);
|
||||
|
||||
using var inventoryDoc = JsonDocument.Parse(result.Inventory.JsonBytes);
|
||||
var inventoryRoot = inventoryDoc.RootElement;
|
||||
Assert.True(inventoryRoot.TryGetProperty("vulnerabilities", out var inventoryVulnerabilities));
|
||||
var inventoryVulns = inventoryVulnerabilities.EnumerateArray().ToArray();
|
||||
Assert.Equal(2, inventoryVulns.Length);
|
||||
|
||||
var primaryVuln = inventoryVulns.Single(v => string.Equals(v.GetProperty("bom-ref").GetString(), "finding-a", StringComparison.Ordinal));
|
||||
var primaryProperties = primaryVuln.GetProperty("properties")
|
||||
.EnumerateArray()
|
||||
.ToDictionary(
|
||||
element => element.GetProperty("name").GetString()!,
|
||||
element => element.GetProperty("value").GetString()!,
|
||||
StringComparer.Ordinal);
|
||||
Assert.Equal("Blocked", primaryProperties["stellaops:policy.status"]);
|
||||
Assert.Equal("true", primaryProperties["stellaops:policy.quiet"]);
|
||||
Assert.Equal("40.5", primaryProperties["stellaops:policy.score"]);
|
||||
Assert.Equal("medium", primaryProperties["stellaops:policy.confidenceBand"]);
|
||||
Assert.Equal("runtime", primaryProperties["stellaops:policy.reachability"]);
|
||||
Assert.Equal("0.45", primaryProperties["stellaops:policy.input.reachabilityWeight"]);
|
||||
var ratingScore = primaryVuln.GetProperty("ratings").EnumerateArray().Single().GetProperty("score").GetDouble();
|
||||
Assert.Equal(40.5, ratingScore);
|
||||
|
||||
using var usageDoc = JsonDocument.Parse(result.Usage.JsonBytes);
|
||||
var usageRoot = usageDoc.RootElement;
|
||||
Assert.True(usageRoot.TryGetProperty("vulnerabilities", out var usageVulnerabilities));
|
||||
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);
|
||||
|
||||
Assert.Equal(first.Inventory.JsonSha256, second.Inventory.JsonSha256);
|
||||
Assert.Equal(first.Inventory.ProtobufSha256, second.Inventory.ProtobufSha256);
|
||||
Assert.Equal(first.Inventory.SerialNumber, second.Inventory.SerialNumber);
|
||||
|
||||
Assert.NotNull(first.Usage);
|
||||
Assert.NotNull(second.Usage);
|
||||
Assert.Equal(first.Usage!.JsonSha256, second.Usage!.JsonSha256);
|
||||
Assert.Equal(first.Usage.ProtobufSha256, second.Usage.ProtobufSha256);
|
||||
Assert.Equal(first.Usage.SerialNumber, second.Usage.SerialNumber);
|
||||
}
|
||||
|
||||
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",
|
||||
Licenses = new[] { "MIT" },
|
||||
Properties = new Dictionary<string, string>
|
||||
{
|
||||
["stellaops:source"] = "package-lock.json",
|
||||
["stellaops.os.analyzer"] = "apk",
|
||||
["stellaops.os.architecture"] = "x86_64",
|
||||
},
|
||||
BuildId = "ABCDEF1234567890ABCDEF1234567890ABCDEF12",
|
||||
},
|
||||
}
|
||||
}),
|
||||
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,
|
||||
new DateTimeOffset(2025, 10, 19, 12, 0, 0, TimeSpan.Zero),
|
||||
generatorName: "StellaOps.Scanner",
|
||||
generatorVersion: "0.10.0",
|
||||
properties: new Dictionary<string, string>
|
||||
{
|
||||
["stellaops:scanId"] = "scan-1234",
|
||||
},
|
||||
policyFindings: new[]
|
||||
{
|
||||
new SbomPolicyFinding
|
||||
{
|
||||
FindingId = "finding-a",
|
||||
ComponentKey = "pkg:npm/a",
|
||||
VulnerabilityId = "CVE-2025-0001",
|
||||
Status = "Blocked",
|
||||
Score = 40.5,
|
||||
ConfigVersion = "1.0",
|
||||
Quiet = true,
|
||||
QuietedBy = "policy/quiet-critical-runtime",
|
||||
UnknownConfidence = 0.42,
|
||||
ConfidenceBand = "medium",
|
||||
UnknownAgeDays = 5,
|
||||
SourceTrust = "NVD",
|
||||
Reachability = "runtime",
|
||||
Inputs = ImmutableArray.Create(
|
||||
new KeyValuePair<string, double>("severityWeight", 90),
|
||||
new KeyValuePair<string, double>("trustWeight", 1.0),
|
||||
new KeyValuePair<string, double>("reachabilityWeight", 0.45))
|
||||
},
|
||||
new SbomPolicyFinding
|
||||
{
|
||||
FindingId = "finding-b",
|
||||
ComponentKey = "pkg:npm/b",
|
||||
VulnerabilityId = "CVE-2025-0002",
|
||||
Status = "Warned",
|
||||
Score = 12.5,
|
||||
ConfigVersion = "1.0",
|
||||
Quiet = false,
|
||||
SourceTrust = "StellaOps",
|
||||
Reachability = "indirect",
|
||||
Inputs = ImmutableArray.Create(
|
||||
new KeyValuePair<string, double>("severityWeight", 55),
|
||||
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");
|
||||
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);
|
||||
|
||||
var names = components.Select(component => component.GetProperty("name").GetString()!).ToArray();
|
||||
Assert.Equal(names, names.OrderBy(n => n, StringComparer.Ordinal).ToArray());
|
||||
|
||||
var firstComponentProperties = components[0].GetProperty("properties").EnumerateArray().ToDictionary(
|
||||
element => element.GetProperty("name").GetString()!,
|
||||
element => element.GetProperty("value").GetString()!,
|
||||
StringComparer.Ordinal);
|
||||
|
||||
Assert.Equal("apk", firstComponentProperties["stellaops.os.analyzer"]);
|
||||
Assert.Equal("x86_64", firstComponentProperties["stellaops.os.architecture"]);
|
||||
Assert.Equal("abcdef1234567890abcdef1234567890abcdef12", firstComponentProperties["stellaops:buildId"]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user