Files
git.stella-ops.org/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/HeuristicScannerTests.cs

307 lines
9.6 KiB
C#

using System.Text;
using FluentAssertions;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Analyzers.Native.Tests;
public class HeuristicScannerTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Scan_DetectsElfSonamePattern()
{
// Arrange - binary containing soname strings
var data = CreateTestBinaryWithStrings(
"libfoo.so.1",
"libbar.so",
"randomdata");
// Act
using var stream = new MemoryStream(data);
var result = HeuristicScanner.Scan(stream, NativeFormat.Elf);
// Assert
result.Edges.Should().HaveCount(2);
result.Edges.Should().Contain(e => e.LibraryName == "libfoo.so.1");
result.Edges.Should().Contain(e => e.LibraryName == "libbar.so");
result.Edges.Should().OnlyContain(e => e.ReasonCode == HeuristicReasonCodes.StringDlopen);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Scan_DetectsWindowsDllPattern()
{
// Arrange
var data = CreateTestBinaryWithStrings(
"kernel32.dll",
"user32.dll",
"notadll");
// Act
using var stream = new MemoryStream(data);
var result = HeuristicScanner.Scan(stream, NativeFormat.Pe);
// Assert
result.Edges.Should().HaveCount(2);
result.Edges.Should().Contain(e => e.LibraryName == "kernel32.dll");
result.Edges.Should().Contain(e => e.LibraryName == "user32.dll");
result.Edges.Should().OnlyContain(e => e.ReasonCode == HeuristicReasonCodes.StringLoadLibrary);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Scan_DetectsMachODylibPattern()
{
// Arrange
var data = CreateTestBinaryWithStrings(
"libfoo.dylib",
"@rpath/libbar.dylib",
"@loader_path/libbaz.dylib");
// Act
using var stream = new MemoryStream(data);
var result = HeuristicScanner.Scan(stream, NativeFormat.MachO);
// Assert
result.Edges.Should().HaveCount(3);
result.Edges.Should().Contain(e => e.LibraryName == "libfoo.dylib");
result.Edges.Should().Contain(e => e.LibraryName == "@rpath/libbar.dylib");
result.Edges.Should().Contain(e => e.LibraryName == "@loader_path/libbaz.dylib");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Scan_AssignsHighConfidenceToPathLikeStrings()
{
// Arrange
var data = CreateTestBinaryWithStrings(
"/usr/lib/libfoo.so.1",
"libbar.so");
// Act
using var stream = new MemoryStream(data);
var result = HeuristicScanner.Scan(stream, NativeFormat.Elf);
// Assert
var pathLikeEdge = result.Edges.First(e => e.LibraryName == "/usr/lib/libfoo.so.1");
var simpleSoname = result.Edges.First(e => e.LibraryName == "libbar.so");
pathLikeEdge.Confidence.Should().Be(HeuristicConfidence.High);
simpleSoname.Confidence.Should().Be(HeuristicConfidence.Medium);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Scan_DetectsPluginConfigReferences()
{
// Arrange
var data = CreateTestBinaryWithStrings(
"/etc/myapp/plugins.conf",
"config/plugin.json",
"modules.conf");
// Act
using var stream = new MemoryStream(data);
var result = HeuristicScanner.Scan(stream, NativeFormat.Elf);
// Assert
result.PluginConfigs.Should().HaveCount(3);
result.PluginConfigs.Should().Contain("plugins.conf");
result.PluginConfigs.Should().Contain("plugin.json");
result.PluginConfigs.Should().Contain("modules.conf");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Scan_DetectsGoCgoImportDirective()
{
// Arrange - simulate Go binary with cgo import
var marker = Encoding.UTF8.GetBytes("cgo_import_dynamic");
var library = Encoding.UTF8.GetBytes(" libcrypto.so");
var padding = new byte[16];
var data = marker.Concat(library).Concat(padding).ToArray();
// Act
using var stream = new MemoryStream(data);
var result = HeuristicScanner.Scan(stream, NativeFormat.Elf);
// Assert
result.Edges.Should().Contain(e =>
e.LibraryName == "libcrypto.so" &&
e.ReasonCode == HeuristicReasonCodes.GoCgoImport &&
e.Confidence == HeuristicConfidence.High);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Scan_DetectsGoCgoStaticImport()
{
// Arrange
var marker = Encoding.UTF8.GetBytes("cgo_import_static");
var library = Encoding.UTF8.GetBytes(" libz.a");
var padding = new byte[16];
var data = marker.Concat(library).Concat(padding).ToArray();
// Act
using var stream = new MemoryStream(data);
var result = HeuristicScanner.Scan(stream, NativeFormat.Elf);
// Assert
result.Edges.Should().Contain(e =>
e.LibraryName == "libz.a" &&
e.ReasonCode == HeuristicReasonCodes.GoCgoImport);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Scan_DeduplicatesEdgesByLibraryName()
{
// Arrange - same library mentioned multiple times
var data = CreateTestBinaryWithStrings(
"libfoo.so",
"some padding",
"libfoo.so",
"more padding",
"libfoo.so");
// Act
using var stream = new MemoryStream(data);
var result = HeuristicScanner.Scan(stream, NativeFormat.Elf);
// Assert
result.Edges.Should().ContainSingle(e => e.LibraryName == "libfoo.so");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Scan_IncludesFileOffsetInEdge()
{
// Arrange
var prefix = new byte[100];
var libName = Encoding.UTF8.GetBytes("libtest.so");
var suffix = new byte[50];
var data = prefix.Concat(libName).Concat(suffix).ToArray();
// Act
using var stream = new MemoryStream(data);
var result = HeuristicScanner.Scan(stream, NativeFormat.Elf);
// Assert
var edge = result.Edges.First();
edge.FileOffset.Should().Be(100);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ScanForDynamicLoading_ReturnsOnlyLibraryEdges()
{
// Arrange
var data = CreateTestBinaryWithStrings(
"libfoo.so",
"plugins.conf",
"libbar.so");
// Act
var edges = HeuristicScanner.ScanForDynamicLoading(data, NativeFormat.Elf);
// Assert
edges.Should().HaveCount(2);
edges.Should().OnlyContain(e =>
e.ReasonCode == HeuristicReasonCodes.StringDlopen);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ScanForPluginConfigs_ReturnsOnlyConfigReferences()
{
// Arrange
var data = CreateTestBinaryWithStrings(
"libfoo.so",
"/etc/plugins.conf",
"plugin.json",
"libbar.so");
// Act
var configs = HeuristicScanner.ScanForPluginConfigs(data);
// Assert
configs.Should().HaveCount(2);
configs.Should().Contain("plugins.conf");
configs.Should().Contain("plugin.json");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Scan_EmptyStream_ReturnsEmptyResult()
{
// Arrange
using var stream = new MemoryStream([]);
// Act
var result = HeuristicScanner.Scan(stream, NativeFormat.Elf);
// Assert
result.Edges.Should().BeEmpty();
result.PluginConfigs.Should().BeEmpty();
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Scan_NoValidStrings_ReturnsEmptyResult()
{
// Arrange - binary data with no printable strings
var data = new byte[] { 0x00, 0x01, 0x02, 0xFF, 0xFE, 0x80, 0x90 };
// Act
using var stream = new MemoryStream(data);
var result = HeuristicScanner.Scan(stream, NativeFormat.Elf);
// Assert
result.Edges.Should().BeEmpty();
}
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData("libfoo.so.1", true)]
[InlineData("libbar.so", true)]
[InlineData("lib-baz_qux.so.2.3", true)]
[InlineData("libfoo", false)] // Missing .so
[InlineData("foo.so", false)] // Missing lib prefix
[InlineData("lib.so", false)] // Too short
public void Scan_ValidatesElfSonameFormat(string soname, bool shouldMatch)
{
// Arrange
var data = CreateTestBinaryWithStrings(soname);
// Act
using var stream = new MemoryStream(data);
using StellaOps.TestKit;
var result = HeuristicScanner.Scan(stream, NativeFormat.Elf);
// Assert
if (shouldMatch)
{
result.Edges.Should().Contain(e => e.LibraryName == soname);
}
else
{
result.Edges.Should().NotContain(e => e.LibraryName == soname);
}
}
private static byte[] CreateTestBinaryWithStrings(params string[] strings)
{
// Create a test binary with the given strings separated by null bytes
var parts = new List<byte[]>();
foreach (var str in strings)
{
parts.Add(Encoding.UTF8.GetBytes(str));
parts.Add(new byte[] { 0x00, 0x00, 0x00, 0x00 }); // Null separator
}
return parts.SelectMany(p => p).ToArray();
}
}