up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
sdk-generator-smoke / sdk-smoke (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
sdk-generator-smoke / sdk-smoke (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,289 @@
|
||||
using System.Text;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Native.Tests;
|
||||
|
||||
public class HeuristicScannerTests
|
||||
{
|
||||
[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);
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
|
||||
[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");
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
|
||||
[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");
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
|
||||
[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");
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
|
||||
[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");
|
||||
}
|
||||
|
||||
[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();
|
||||
}
|
||||
|
||||
[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();
|
||||
}
|
||||
|
||||
[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);
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user