This commit is contained in:
StellaOps Bot
2025-12-13 02:22:15 +02:00
parent 564df71bfb
commit 999e26a48e
395 changed files with 25045 additions and 2224 deletions

View File

@@ -156,7 +156,7 @@ public class PeImportParserTests : NativeTestBase
// Test that manifest is properly extracted from PE resources
var pe = PeBuilder.Console64()
.WithSxsDependency("Microsoft.VC90.CRT", "9.0.21022.8",
"1fc8b3b9a1e18e3b", "amd64", embedAsResource: true)
"1fc8b3b9a1e18e3b", "amd64")
.Build();
var info = ParsePe(pe);

View File

@@ -0,0 +1,591 @@
using System.Collections.Immutable;
using System.Security.Cryptography;
using System.Text;
using FluentAssertions;
using Xunit;
namespace StellaOps.Scanner.Analyzers.Native.Tests.Reachability;
/// <summary>
/// Tests ensuring native callgraph output conforms to richgraph-v1 schema.
/// Per docs/modules/scanner/design/native-reachability-plan.md §8.
/// These tests validate the expected identifier formats and behaviors
/// defined in the specification.
/// </summary>
public class RichgraphV1AlignmentTests
{
/// <summary>
/// §8.2: SymbolID Construction uses sym: prefix with 16 hex chars.
/// </summary>
[Theory]
[InlineData("ssl3_read_bytes", 0x401000UL, 256UL, "global")]
[InlineData("main", 0x400000UL, 128UL, "global")]
[InlineData("helper", 0x402000UL, 64UL, "local")]
public void ComputeSymbolId_UsesBinaryPrefix(string name, ulong address, ulong size, string binding)
{
// Act
var symbolId = TestGraphIdentifiers.ComputeSymbolId(name, address, size, binding);
// Assert
symbolId.Should().StartWith("sym:");
symbolId.Should().HaveLength(20); // sym: (4 chars) + 16 hex chars = 20
}
/// <summary>
/// §8.5: Symbol digest uses SHA-256.
/// </summary>
[Theory]
[InlineData("ssl3_read_bytes", 0x401000UL, 256UL, "global")]
[InlineData("main", 0x400000UL, 128UL, "global")]
public void ComputeSymbolDigest_UsesSha256(string name, ulong address, ulong size, string binding)
{
// Act
var digest = TestGraphIdentifiers.ComputeSymbolDigest(name, address, size, binding);
// Assert
digest.Should().HaveLength(64); // SHA-256 produces 64 hex chars
digest.Should().MatchRegex("^[a-f0-9]{64}$");
}
/// <summary>
/// SymbolID is deterministic for the same inputs.
/// </summary>
[Fact]
public void ComputeSymbolId_IsDeterministic()
{
// Arrange
var name = "test_function";
var address = 0x400100UL;
var size = 100UL;
var binding = "global";
// Act
var id1 = TestGraphIdentifiers.ComputeSymbolId(name, address, size, binding);
var id2 = TestGraphIdentifiers.ComputeSymbolId(name, address, size, binding);
// Assert
id1.Should().Be(id2);
}
/// <summary>
/// SymbolID differs when inputs differ.
/// </summary>
[Fact]
public void ComputeSymbolId_DiffersForDifferentInputs()
{
// Act
var id1 = TestGraphIdentifiers.ComputeSymbolId("func_a", 0x1000, 100, "global");
var id2 = TestGraphIdentifiers.ComputeSymbolId("func_b", 0x1000, 100, "global");
var id3 = TestGraphIdentifiers.ComputeSymbolId("func_a", 0x2000, 100, "global");
// Assert
id1.Should().NotBe(id2);
id1.Should().NotBe(id3);
id2.Should().NotBe(id3);
}
/// <summary>
/// EdgeID construction is deterministic.
/// </summary>
[Fact]
public void ComputeEdgeId_IsDeterministic()
{
// Arrange
var callerId = "sym:abc123";
var calleeId = "sym:def456";
var offset = 0x100UL;
// Act
var id1 = TestGraphIdentifiers.ComputeEdgeId(callerId, calleeId, offset);
var id2 = TestGraphIdentifiers.ComputeEdgeId(callerId, calleeId, offset);
// Assert
id1.Should().Be(id2);
id1.Should().StartWith("edge:");
}
/// <summary>
/// §8.5: RootID for init array uses correct format.
/// </summary>
[Fact]
public void ComputeRootId_UsesCorrectFormat()
{
// Arrange
var targetId = "sym:abc123";
var rootType = TestRootType.InitArray;
var order = 0;
// Act
var rootId = TestGraphIdentifiers.ComputeRootId(targetId, rootType, order);
// Assert
rootId.Should().StartWith("root:");
rootId.Should().HaveLength(21); // root: (5 chars) + 16 hex chars = 21
}
/// <summary>
/// §8.8: UnknownID for unresolved targets.
/// </summary>
[Fact]
public void ComputeUnknownId_UsesCorrectFormat()
{
// Arrange
var sourceId = "edge:abc123";
var unknownType = TestUnknownType.UnresolvedTarget;
var name = "dlopen_target";
// Act
var unknownId = TestGraphIdentifiers.ComputeUnknownId(sourceId, unknownType, name);
// Assert
unknownId.Should().StartWith("unk:");
unknownId.Should().HaveLength(20); // unk: (4 chars) + 16 hex chars = 20
}
/// <summary>
/// Graph hash is deterministic for same content.
/// </summary>
[Fact]
public void ComputeGraphHash_IsDeterministic()
{
// Arrange
var functions = new[]
{
new TestFunctionNode("sym:001", "func_a", null, "/bin/app", null, 0x1000, 100, "digest1", "global", "default", true),
new TestFunctionNode("sym:002", "func_b", null, "/bin/app", null, 0x2000, 100, "digest2", "global", "default", true),
}.ToImmutableArray();
var edges = new[]
{
new TestCallEdge("edge:001", "sym:001", "sym:002", null, null, TestEdgeType.Direct, 0x50, true, 1.0),
}.ToImmutableArray();
var roots = new[]
{
new TestSyntheticRoot("root:001", "sym:001", TestRootType.Main, "/bin/app", "main", 0),
}.ToImmutableArray();
// Act
var hash1 = TestGraphIdentifiers.ComputeGraphHash(functions, edges, roots);
var hash2 = TestGraphIdentifiers.ComputeGraphHash(functions, edges, roots);
// Assert
hash1.Should().Be(hash2);
hash1.Should().HaveLength(64); // SHA-256
}
/// <summary>
/// Graph hash changes when content changes.
/// </summary>
[Fact]
public void ComputeGraphHash_ChangesWhenContentChanges()
{
// Arrange
var functions1 = new[]
{
new TestFunctionNode("sym:001", "func_a", null, "/bin/app", null, 0x1000, 100, "digest1", "global", "default", true),
}.ToImmutableArray();
var functions2 = new[]
{
new TestFunctionNode("sym:002", "func_b", null, "/bin/app", null, 0x2000, 100, "digest2", "global", "default", true),
}.ToImmutableArray();
var emptyEdges = ImmutableArray<TestCallEdge>.Empty;
var emptyRoots = ImmutableArray<TestSyntheticRoot>.Empty;
// Act
var hash1 = TestGraphIdentifiers.ComputeGraphHash(functions1, emptyEdges, emptyRoots);
var hash2 = TestGraphIdentifiers.ComputeGraphHash(functions2, emptyEdges, emptyRoots);
// Assert
hash1.Should().NotBe(hash2);
}
/// <summary>
/// §8.4: Edge kind mapping for PLT calls.
/// </summary>
[Fact]
public void EdgeType_Plt_MapsToCall()
{
// Arrange
var edge = new TestCallEdge(
"edge:001",
"sym:caller",
"sym:callee",
"pkg:deb/ubuntu/openssl@3.0.2",
"sha256:abc123",
TestEdgeType.Plt,
0x100,
true,
0.95);
// Assert
edge.EdgeType.Should().Be(TestEdgeType.Plt);
edge.Confidence.Should().Be(0.95); // PLT resolved confidence
}
/// <summary>
/// §8.4: Edge kind mapping for init array.
/// </summary>
[Fact]
public void EdgeType_InitArray_MapsToInit()
{
// Arrange
var edge = new TestCallEdge(
"edge:002",
"sym:init",
"sym:constructor",
null,
null,
TestEdgeType.InitArray,
0,
true,
1.0);
// Assert
edge.EdgeType.Should().Be(TestEdgeType.InitArray);
edge.Confidence.Should().Be(1.0); // Init array entries have high confidence
}
/// <summary>
/// §8.5: Synthetic roots for native entry points.
/// </summary>
[Theory]
[InlineData(TestRootType.Start, "load")]
[InlineData(TestRootType.Main, "main")]
[InlineData(TestRootType.Init, "init")]
[InlineData(TestRootType.InitArray, "init")]
[InlineData(TestRootType.PreInitArray, "preinit")]
[InlineData(TestRootType.Fini, "fini")]
[InlineData(TestRootType.FiniArray, "fini")]
public void SyntheticRoot_HasCorrectPhase(TestRootType rootType, string expectedPhase)
{
// This test verifies the expected phase mapping - actual implementation
// may use different phase strings, but the mapping should be documented
var root = new TestSyntheticRoot(
"root:001",
"sym:target",
rootType,
"/bin/app",
expectedPhase,
0);
root.Phase.Should().Be(expectedPhase);
}
/// <summary>
/// §8.7: Stripped binary handling - synthetic name format.
/// </summary>
[Theory]
[InlineData(0x401000UL, "sub_401000")]
[InlineData(0x402000UL, "sub_402000")]
[InlineData(0x500ABCUL, "sub_500abc")]
public void StrippedSymbol_UsesSubAddressFormat(ulong address, string expectedName)
{
// Act
var syntheticName = $"sub_{address:x}";
// Assert
syntheticName.Should().Be(expectedName);
}
/// <summary>
/// §8.6: Build ID handling for ELF.
/// </summary>
[Fact]
public void BuildId_FormatForElf()
{
// Arrange
var elfBuildId = "a1b2c3d4e5f6";
// Act
var formattedBuildId = $"gnu-build-id:{elfBuildId}";
// Assert
formattedBuildId.Should().Be("gnu-build-id:a1b2c3d4e5f6");
formattedBuildId.Should().StartWith("gnu-build-id:");
}
/// <summary>
/// §8.8: Unknown edge targets with candidates.
/// </summary>
[Fact]
public void UnknownTarget_HasLowConfidence()
{
// Arrange
var unknownEdge = new TestCallEdge(
"edge:003",
"sym:caller",
"unknown:plt_42",
null,
null,
TestEdgeType.Indirect,
0x200,
false,
0.3); // Low confidence for unknown targets
// Assert
unknownEdge.IsResolved.Should().BeFalse();
unknownEdge.Confidence.Should().BeLessThan(0.5);
unknownEdge.EdgeType.Should().Be(TestEdgeType.Indirect);
}
/// <summary>
/// TestFunctionNode contains all required fields.
/// </summary>
[Fact]
public void TestFunctionNode_HasRequiredFields()
{
// Arrange & Act
var node = new TestFunctionNode(
SymbolId: "sym:binary:abc123",
Name: "ssl3_read_bytes",
Purl: "pkg:deb/ubuntu/openssl@3.0.2?arch=amd64",
BinaryPath: "/usr/lib/libssl.so.3",
BuildId: "gnu-build-id:a1b2c3d4e5f6",
Address: 0x401000,
Size: 256,
SymbolDigest: "sha256:deadbeef",
Binding: "global",
Visibility: "default",
IsExported: true);
// Assert - all fields present per richgraph-v1 §8.1
node.SymbolId.Should().NotBeNullOrEmpty();
node.Name.Should().NotBeNullOrEmpty();
node.Purl.Should().NotBeNullOrEmpty();
node.BinaryPath.Should().NotBeNullOrEmpty();
node.BuildId.Should().NotBeNullOrEmpty();
node.Address.Should().BeGreaterThan(0);
node.Size.Should().BeGreaterThan(0);
node.SymbolDigest.Should().NotBeNullOrEmpty();
node.Binding.Should().NotBeNullOrEmpty();
node.Visibility.Should().NotBeNullOrEmpty();
}
/// <summary>
/// TestCallEdge contains all required fields.
/// </summary>
[Fact]
public void TestCallEdge_HasRequiredFields()
{
// Arrange & Act
var edge = new TestCallEdge(
EdgeId: "edge:binary:abc123",
CallerId: "sym:binary:caller",
CalleeId: "sym:binary:callee",
CalleePurl: "pkg:deb/ubuntu/openssl@3.0.2",
CalleeSymbolDigest: "sha256:cafebabe",
EdgeType: TestEdgeType.Plt,
CallSiteOffset: 0x100,
IsResolved: true,
Confidence: 0.95);
// Assert - all fields present per richgraph-v1 §8.3
edge.EdgeId.Should().NotBeNullOrEmpty();
edge.CallerId.Should().NotBeNullOrEmpty();
edge.CalleeId.Should().NotBeNullOrEmpty();
edge.CalleePurl.Should().NotBeNullOrEmpty();
edge.CalleeSymbolDigest.Should().NotBeNullOrEmpty();
edge.CallSiteOffset.Should().BeGreaterThan(0);
edge.Confidence.Should().BeInRange(0, 1);
}
/// <summary>
/// TestGraphMetadata contains generation info.
/// </summary>
[Fact]
public void TestGraphMetadata_HasGeneratorInfo()
{
// Arrange & Act
var metadata = new TestGraphMetadata(
GeneratedAt: DateTimeOffset.UtcNow,
GeneratorVersion: "1.0.0",
LayerDigest: "sha256:layer123",
BinaryCount: 5,
FunctionCount: 100,
EdgeCount: 250,
UnknownCount: 10,
SyntheticRootCount: 8);
// Assert
metadata.GeneratedAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromMinutes(1));
metadata.GeneratorVersion.Should().Be("1.0.0");
metadata.LayerDigest.Should().StartWith("sha256:");
metadata.BinaryCount.Should().BeGreaterThan(0);
metadata.FunctionCount.Should().BeGreaterThan(0);
metadata.EdgeCount.Should().BeGreaterThan(0);
}
/// <summary>
/// Generator version is retrievable.
/// </summary>
[Fact]
public void GetGeneratorVersion_ReturnsSemanticVersion()
{
// Act
var version = TestGraphIdentifiers.GetGeneratorVersion();
// Assert
version.Should().MatchRegex(@"^\d+\.\d+\.\d+$");
}
#region Test Model Definitions (mirror richgraph-v1 schema)
/// <summary>Test model mirroring NativeFunctionNode.</summary>
internal sealed record TestFunctionNode(
string SymbolId,
string Name,
string? Purl,
string BinaryPath,
string? BuildId,
ulong Address,
ulong Size,
string SymbolDigest,
string Binding,
string Visibility,
bool IsExported);
/// <summary>Test model mirroring NativeCallEdge.</summary>
internal sealed record TestCallEdge(
string EdgeId,
string CallerId,
string CalleeId,
string? CalleePurl,
string? CalleeSymbolDigest,
TestEdgeType EdgeType,
ulong CallSiteOffset,
bool IsResolved,
double Confidence);
/// <summary>Test model mirroring NativeSyntheticRoot.</summary>
internal sealed record TestSyntheticRoot(
string RootId,
string TargetId,
TestRootType RootType,
string BinaryPath,
string Phase,
int Order);
/// <summary>Test model mirroring NativeGraphMetadata.</summary>
internal sealed record TestGraphMetadata(
DateTimeOffset GeneratedAt,
string GeneratorVersion,
string LayerDigest,
int BinaryCount,
int FunctionCount,
int EdgeCount,
int UnknownCount,
int SyntheticRootCount);
/// <summary>Test enum mirroring NativeEdgeType.</summary>
public enum TestEdgeType
{
Direct,
Plt,
Got,
Relocation,
Indirect,
InitArray,
FiniArray,
}
/// <summary>Test enum mirroring NativeRootType.</summary>
public enum TestRootType
{
Start,
Init,
PreInitArray,
InitArray,
FiniArray,
Fini,
Main,
Constructor,
Destructor,
}
/// <summary>Test enum mirroring NativeUnknownType.</summary>
public enum TestUnknownType
{
UnresolvedPurl,
UnresolvedTarget,
UnresolvedHash,
UnresolvedBinary,
AmbiguousTarget,
}
/// <summary>
/// Test implementation of identifier computation methods.
/// These mirror the expected behavior defined in richgraph-v1 schema.
/// </summary>
internal static class TestGraphIdentifiers
{
private const string GeneratorVersion = "1.0.0";
public static string ComputeSymbolId(string name, ulong address, ulong size, string binding)
{
var input = $"{name}:{address:x}:{size}:{binding}";
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
return $"sym:{Convert.ToHexString(hash[..8]).ToLowerInvariant()}";
}
public static string ComputeSymbolDigest(string name, ulong address, ulong size, string binding)
{
var input = $"{name}:{address:x}:{size}:{binding}";
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
return Convert.ToHexString(hash).ToLowerInvariant();
}
public static string ComputeEdgeId(string callerId, string calleeId, ulong callSiteOffset)
{
var input = $"{callerId}:{calleeId}:{callSiteOffset:x}";
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
return $"edge:{Convert.ToHexString(hash[..8]).ToLowerInvariant()}";
}
public static string ComputeRootId(string targetId, TestRootType rootType, int order)
{
var input = $"{targetId}:{rootType}:{order}";
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
return $"root:{Convert.ToHexString(hash[..8]).ToLowerInvariant()}";
}
public static string ComputeUnknownId(string sourceId, TestUnknownType unknownType, string? name)
{
var input = $"{sourceId}:{unknownType}:{name ?? ""}";
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
return $"unk:{Convert.ToHexString(hash[..8]).ToLowerInvariant()}";
}
public static string ComputeGraphHash(
ImmutableArray<TestFunctionNode> functions,
ImmutableArray<TestCallEdge> edges,
ImmutableArray<TestSyntheticRoot> roots)
{
using var sha = IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
foreach (var f in functions.OrderBy(f => f.SymbolId))
{
sha.AppendData(Encoding.UTF8.GetBytes(f.SymbolId));
sha.AppendData(Encoding.UTF8.GetBytes(f.SymbolDigest));
}
foreach (var e in edges.OrderBy(e => e.EdgeId))
{
sha.AppendData(Encoding.UTF8.GetBytes(e.EdgeId));
}
foreach (var r in roots.OrderBy(r => r.RootId))
{
sha.AppendData(Encoding.UTF8.GetBytes(r.RootId));
}
return Convert.ToHexString(sha.GetCurrentHash()).ToLowerInvariant();
}
public static string GetGeneratorVersion() => GeneratorVersion;
}
#endregion
}