This commit is contained in:
StellaOps Bot
2025-12-14 23:20:14 +02:00
parent 3411e825cd
commit b058dbe031
356 changed files with 68310 additions and 1108 deletions

View File

@@ -0,0 +1,140 @@
using System.Collections.Generic;
using System.Linq;
using StellaOps.Scanner.Reachability.Ordering;
using Xunit;
namespace StellaOps.Scanner.Reachability.Tests;
public sealed class DeterministicGraphOrdererTests
{
[Fact]
public void Canonicalize_IsDeterministic_AcrossInputOrdering()
{
var orderer = new DeterministicGraphOrderer();
var graph1 = new RichGraph(
Nodes: new[] { Node("B"), Node("A"), Node("C") },
Edges: new[] { Edge("A", "C"), Edge("A", "B"), Edge("B", "C") },
Roots: new[] { new RichGraphRoot("A", "runtime", null) },
Analyzer: new RichGraphAnalyzer("test", "1.0", null));
var graph2 = new RichGraph(
Nodes: new[] { Node("C"), Node("A"), Node("B") },
Edges: new[] { Edge("B", "C"), Edge("A", "B"), Edge("A", "C") },
Roots: new[] { new RichGraphRoot("A", "runtime", null) },
Analyzer: new RichGraphAnalyzer("test", "1.0", null));
var canonical1 = orderer.Canonicalize(graph1, GraphOrderingStrategy.TopologicalLexicographic);
var canonical2 = orderer.Canonicalize(graph2, GraphOrderingStrategy.TopologicalLexicographic);
Assert.Equal(canonical1.ContentHash, canonical2.ContentHash);
Assert.Equal(canonical1.Nodes.Select(n => n.Id), canonical2.Nodes.Select(n => n.Id));
Assert.Equal(canonical1.Edges.Select(e => (e.SourceIndex, e.TargetIndex, e.EdgeType)),
canonical2.Edges.Select(e => (e.SourceIndex, e.TargetIndex, e.EdgeType)));
}
[Fact]
public void TopologicalLexicographic_UsesLexicographicTiebreakers()
{
var orderer = new DeterministicGraphOrderer();
var graph = new RichGraph(
Nodes: new[] { Node("C"), Node("B"), Node("A") },
Edges: new[] { Edge("A", "C"), Edge("B", "C") },
Roots: Array.Empty<RichGraphRoot>(),
Analyzer: new RichGraphAnalyzer("test", "1.0", null));
var order = orderer.OrderNodes(graph, GraphOrderingStrategy.TopologicalLexicographic);
Assert.Equal(new[] { "A", "B", "C" }, order);
}
[Fact]
public void TopologicalLexicographic_HandlesCyclesByAppendingRemainder()
{
var orderer = new DeterministicGraphOrderer();
var graph = new RichGraph(
Nodes: new[] { Node("B"), Node("A"), Node("C") },
Edges: new[] { Edge("A", "B"), Edge("B", "A") },
Roots: Array.Empty<RichGraphRoot>(),
Analyzer: new RichGraphAnalyzer("test", "1.0", null));
var order = orderer.OrderNodes(graph, GraphOrderingStrategy.TopologicalLexicographic);
Assert.Equal(new[] { "C", "A", "B" }, order);
}
[Fact]
public void BreadthFirstLexicographic_TraversesFromAnchors()
{
var orderer = new DeterministicGraphOrderer();
var graph = new RichGraph(
Nodes: new[] { Node("D"), Node("C"), Node("B"), Node("A") },
Edges: new[] { Edge("A", "C"), Edge("A", "B"), Edge("B", "D") },
Roots: new[] { new RichGraphRoot("A", "runtime", null) },
Analyzer: new RichGraphAnalyzer("test", "1.0", null));
var order = orderer.OrderNodes(graph, GraphOrderingStrategy.BreadthFirstLexicographic);
Assert.Equal(new[] { "A", "B", "C", "D" }, order);
}
[Fact]
public void DepthFirstLexicographic_TraversesFromAnchors()
{
var orderer = new DeterministicGraphOrderer();
var graph = new RichGraph(
Nodes: new[] { Node("D"), Node("C"), Node("B"), Node("A") },
Edges: new[] { Edge("A", "C"), Edge("A", "B"), Edge("B", "D") },
Roots: new[] { new RichGraphRoot("A", "runtime", null) },
Analyzer: new RichGraphAnalyzer("test", "1.0", null));
var order = orderer.OrderNodes(graph, GraphOrderingStrategy.DepthFirstLexicographic);
Assert.Equal(new[] { "A", "B", "D", "C" }, order);
}
[Fact]
public void OrderEdges_SortsByNodeOrderThenKind()
{
var orderer = new DeterministicGraphOrderer();
var graph = new RichGraph(
Nodes: new[] { Node("C"), Node("B"), Node("A") },
Edges: new[]
{
Edge("B", "C", kind: "import"),
Edge("A", "B", kind: "call"),
Edge("A", "C", kind: "call")
},
Roots: Array.Empty<RichGraphRoot>(),
Analyzer: new RichGraphAnalyzer("test", "1.0", null));
var order = new[] { "A", "B", "C" };
var edges = orderer.OrderEdges(graph, order).ToList();
Assert.Equal(("A", "B", "call"), (edges[0].From, edges[0].To, edges[0].Kind));
Assert.Equal(("A", "C", "call"), (edges[1].From, edges[1].To, edges[1].Kind));
Assert.Equal(("B", "C", "import"), (edges[2].From, edges[2].To, edges[2].Kind));
}
private static RichGraphNode Node(string id, IReadOnlyDictionary<string, string>? attributes = null)
=> new(
Id: id,
SymbolId: id,
CodeId: null,
Purl: null,
Lang: "dotnet",
Kind: "method",
Display: id,
BuildId: null,
Evidence: Array.Empty<string>(),
Attributes: attributes,
SymbolDigest: null);
private static RichGraphEdge Edge(string from, string to, string kind = "call")
=> new(
From: from,
To: to,
Kind: kind,
Purl: null,
SymbolDigest: null,
Evidence: Array.Empty<string>(),
Confidence: 1.0,
Candidates: Array.Empty<string>());
}

View File

@@ -0,0 +1,27 @@
using StellaOps.Scanner.Reachability;
using Xunit;
namespace StellaOps.Scanner.Reachability.Tests;
public sealed class SinkRegistryTests
{
[Theory]
[InlineData("dotnet", "System.Diagnostics.Process.Start", SinkCategory.CmdExec)]
[InlineData("dotnet", "SYSTEM.DIAGNOSTICS.PROCESS.START", SinkCategory.CmdExec)]
[InlineData("java", "java.io.ObjectInputStream.readObject", SinkCategory.UnsafeDeser)]
[InlineData("node", "child_process.exec", SinkCategory.CmdExec)]
[InlineData("python", "pickle.loads", SinkCategory.UnsafeDeser)]
public void MatchSink_ReturnsExpectedCategory(string language, string symbol, SinkCategory expectedCategory)
{
var sink = SinkRegistry.MatchSink(language, symbol);
Assert.NotNull(sink);
Assert.Equal(expectedCategory, sink!.Category);
}
[Fact]
public void MatchSink_ReturnsNull_WhenUnknownLanguage()
{
Assert.Null(SinkRegistry.MatchSink("unknown", "whatever"));
}
}

View File

@@ -0,0 +1,86 @@
{
"schemaVersion": "1.0.0",
"baseImage": {
"digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"name": "example/base",
"tag": "1.0"
},
"targetImage": {
"digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"name": "example/target",
"tag": "2.0"
},
"diff": {
"filesAdded": [
"./b.txt",
"./a.txt"
],
"filesRemoved": [
"./z.txt"
],
"filesChanged": [
{
"path": "./src/app.cs",
"hunks": [
{
"startLine": 1,
"lineCount": 2,
"content": "changed"
}
],
"fromHash": "old",
"toHash": "new"
}
],
"packagesChanged": [
{
"name": "openssl",
"from": "1.1.1u",
"to": "3.0.14",
"purl": "pkg:deb/openssl@3.0.14"
}
],
"packagesAdded": [
{
"name": "curl",
"version": "8.5.0",
"purl": "pkg:deb/curl@8.5.0"
}
],
"packagesRemoved": [
{
"name": "wget",
"version": "1.21.1",
"purl": "pkg:deb/wget@1.21.1"
}
]
},
"reachabilityGate": {
"reachable": true,
"configActivated": true,
"runningUser": false,
"class": 6,
"rationale": "fixture"
},
"scanner": {
"name": "StellaOps.Scanner",
"version": "10.0.0",
"ruleset": "reachability-2025.12"
},
"context": {
"entrypoint": [
"/app/start"
],
"env": {
"FEATURE_X": "true"
},
"user": {
"uid": 1001,
"caps": [
"NET_BIND_SERVICE"
]
}
},
"suppressedCount": 0
}

View File

@@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using StellaOps.Scanner.SmartDiff;
using Xunit;
namespace StellaOps.Scanner.SmartDiff.Tests;
public sealed class PredicateGoldenFixtureTests
{
[Fact]
public void Serialize_MatchesGoldenFixture()
{
var predicate = new SmartDiffPredicate(
SchemaVersion: SmartDiffPredicate.CurrentSchemaVersion,
BaseImage: new ImageReference(Digest: "sha256:" + new string('a', 64), Name: "example/base", Tag: "1.0"),
TargetImage: new ImageReference(Digest: "sha256:" + new string('b', 64), Name: "example/target", Tag: "2.0"),
Diff: new DiffPayload(
FilesAdded: ["./b.txt", "./a.txt"],
FilesRemoved: ["./z.txt"],
FilesChanged:
[
new FileChange("./src/app.cs", Hunks: [new DiffHunk(1, 2, "changed")], FromHash: "old", ToHash: "new"),
],
PackagesChanged:
[
new PackageChange("openssl", From: "1.1.1u", To: "3.0.14", Purl: "pkg:deb/openssl@3.0.14"),
],
PackagesAdded:
[
new PackageRef("curl", Version: "8.5.0", Purl: "pkg:deb/curl@8.5.0"),
],
PackagesRemoved:
[
new PackageRef("wget", Version: "1.21.1", Purl: "pkg:deb/wget@1.21.1"),
]),
ReachabilityGate: ReachabilityGate.Create(reachable: true, configActivated: true, runningUser: false, rationale: "fixture"),
Scanner: new ScannerInfo(Name: "StellaOps.Scanner", Version: "10.0.0", Ruleset: "reachability-2025.12"),
Context: new RuntimeContext(
Entrypoint: ["/app/start"],
Env: ImmutableDictionary.CreateRange(new[]
{
new KeyValuePair<string, string>("FEATURE_X", "true"),
}),
User: new UserContext(Uid: 1001, Caps: ["NET_BIND_SERVICE"])),
SuppressedCount: 0,
MaterialChanges: null);
var json = SmartDiffJsonSerializer.Serialize(predicate, indent: true);
var fixturePath = Path.Combine(AppContext.BaseDirectory, "Fixtures", "smart-diff-predicate.v1.json");
var expected = File.ReadAllText(fixturePath, Encoding.UTF8);
Assert.Equal(Normalize(expected), Normalize(json));
}
[Fact]
public void Serialize_UsesSnakeCaseEnumMemberNames()
{
var change = new MaterialChange(
FindingKey: new FindingKey("pkg:npm/example", "1.0.0", "CVE-2025-1234"),
ChangeType: MaterialChangeType.ReachabilityFlip,
Reason: "test");
var json = JsonSerializer.Serialize(change, new JsonSerializerOptions
{
Converters =
{
new JsonStringEnumConverter(),
}
});
using var parsed = JsonDocument.Parse(json);
Assert.Equal("reachability_flip", parsed.RootElement.GetProperty("changeType").GetString());
}
private static string Normalize(string input)
=> input.Replace("\r\n", "\n", StringComparison.Ordinal).Trim();
}

View File

@@ -0,0 +1,57 @@
using System.Text.Json;
using StellaOps.Scanner.SmartDiff;
using Xunit;
namespace StellaOps.Scanner.SmartDiff.Tests;
public sealed class ReachabilityGateTests
{
[Theory]
[InlineData(false, false, false, 0)]
[InlineData(false, false, true, 1)]
[InlineData(false, true, false, 2)]
[InlineData(false, true, true, 3)]
[InlineData(true, false, false, 4)]
[InlineData(true, false, true, 5)]
[InlineData(true, true, false, 6)]
[InlineData(true, true, true, 7)]
public void ComputeClass_Returns0To7_WhenAllKnown(bool reachable, bool configActivated, bool runningUser, int expected)
{
Assert.Equal(expected, ReachabilityGate.ComputeClass(reachable, configActivated, runningUser));
}
[Theory]
[InlineData(null, false, false)]
[InlineData(false, null, false)]
[InlineData(false, false, null)]
[InlineData(null, null, false)]
[InlineData(null, false, null)]
[InlineData(false, null, null)]
[InlineData(null, null, null)]
public void ComputeClass_ReturnsMinus1_WhenAnyUnknown(bool? reachable, bool? configActivated, bool? runningUser)
{
Assert.Equal(-1, ReachabilityGate.ComputeClass(reachable, configActivated, runningUser));
}
[Fact]
public void Serialize_UsesSchemaFieldNames()
{
var gate = ReachabilityGate.Create(
reachable: true,
configActivated: false,
runningUser: true,
rationale: "Unit test");
var json = JsonSerializer.Serialize(gate);
using var parsed = JsonDocument.Parse(json);
var root = parsed.RootElement;
Assert.True(root.TryGetProperty("reachable", out _));
Assert.True(root.TryGetProperty("configActivated", out _));
Assert.True(root.TryGetProperty("runningUser", out _));
Assert.True(root.TryGetProperty("class", out var classValue));
Assert.Equal(5, classValue.GetInt32());
Assert.Equal("Unit test", root.GetProperty("rationale").GetString());
}
}

View File

@@ -0,0 +1,24 @@
<?xml version='1.0' encoding='utf-8'?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UseConcelierTestInfra>false</UseConcelierTestInfra>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.SmartDiff/StellaOps.Scanner.SmartDiff.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>