up
This commit is contained in:
@@ -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>());
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user