307 lines
9.6 KiB
C#
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();
|
|
}
|
|
}
|