- Implemented Attestation Chain API client with methods for verifying, fetching, and managing attestation chains. - Created models for Attestation Chain, including DSSE envelope structures and verification results. - Developed Triage Evidence API client for fetching finding evidence, including methods for evidence retrieval by CVE and component. - Added models for Triage Evidence, encapsulating evidence responses, entry points, boundary proofs, and VEX evidence. - Introduced mock implementations for both API clients to facilitate testing and development.
362 lines
9.4 KiB
C#
362 lines
9.4 KiB
C#
using FluentAssertions;
|
|
using StellaOps.Scanner.Analyzers.Native;
|
|
using StellaOps.Scanner.Analyzers.Native.Tests.Fixtures;
|
|
using StellaOps.Scanner.Analyzers.Native.Tests.TestUtilities;
|
|
|
|
namespace StellaOps.Scanner.Analyzers.Native.Tests;
|
|
|
|
/// <summary>
|
|
/// Unit tests for PeReader full PE parsing including CodeView GUID, Rich header, and version resources.
|
|
/// </summary>
|
|
public class PeReaderTests : NativeTestBase
|
|
{
|
|
#region Basic Parsing
|
|
|
|
[Fact]
|
|
public void TryExtractIdentity_InvalidData_ReturnsFalse()
|
|
{
|
|
// Arrange
|
|
var invalidData = new byte[] { 0x00, 0x01, 0x02, 0x03 };
|
|
|
|
// Act
|
|
var result = PeReader.TryExtractIdentity(invalidData, out var identity);
|
|
|
|
// Assert
|
|
result.Should().BeFalse();
|
|
identity.Should().BeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void TryExtractIdentity_TooShort_ReturnsFalse()
|
|
{
|
|
// Arrange
|
|
var shortData = new byte[0x20];
|
|
|
|
// Act
|
|
var result = PeReader.TryExtractIdentity(shortData, out var identity);
|
|
|
|
// Assert
|
|
result.Should().BeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void TryExtractIdentity_MissingMzSignature_ReturnsFalse()
|
|
{
|
|
// Arrange
|
|
var data = new byte[0x100];
|
|
data[0] = (byte)'X';
|
|
data[1] = (byte)'Y';
|
|
|
|
// Act
|
|
var result = PeReader.TryExtractIdentity(data, out var identity);
|
|
|
|
// Assert
|
|
result.Should().BeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void TryExtractIdentity_ValidMinimalPe64_ReturnsTrue()
|
|
{
|
|
// Arrange
|
|
var pe = PeBuilder.Console64().Build();
|
|
|
|
// Act
|
|
var result = PeReader.TryExtractIdentity(pe, out var identity);
|
|
|
|
// Assert
|
|
result.Should().BeTrue();
|
|
identity.Should().NotBeNull();
|
|
identity!.Is64Bit.Should().BeTrue();
|
|
identity.Machine.Should().Be("x86_64");
|
|
identity.Subsystem.Should().Be(PeSubsystem.WindowsConsole);
|
|
}
|
|
|
|
[Fact]
|
|
public void TryExtractIdentity_ValidMinimalPe32_ReturnsTrue()
|
|
{
|
|
// Arrange
|
|
var pe = new PeBuilder()
|
|
.Is64Bit(false)
|
|
.WithSubsystem(PeSubsystem.WindowsConsole)
|
|
.Build();
|
|
|
|
// Act
|
|
var result = PeReader.TryExtractIdentity(pe, out var identity);
|
|
|
|
// Assert
|
|
result.Should().BeTrue();
|
|
identity.Should().NotBeNull();
|
|
identity!.Is64Bit.Should().BeFalse();
|
|
identity.Machine.Should().Be("x86");
|
|
}
|
|
|
|
[Fact]
|
|
public void TryExtractIdentity_GuiSubsystem_ParsesCorrectly()
|
|
{
|
|
// Arrange
|
|
var pe = new PeBuilder()
|
|
.Is64Bit(true)
|
|
.WithSubsystem(PeSubsystem.WindowsGui)
|
|
.Build();
|
|
|
|
// Act
|
|
var result = PeReader.TryExtractIdentity(pe, out var identity);
|
|
|
|
// Assert
|
|
result.Should().BeTrue();
|
|
identity.Should().NotBeNull();
|
|
identity!.Subsystem.Should().Be(PeSubsystem.WindowsGui);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Parse Method
|
|
|
|
[Fact]
|
|
public void Parse_ValidPeStream_ReturnsPeParseResult()
|
|
{
|
|
// Arrange
|
|
var pe = PeBuilder.Console64().Build();
|
|
using var stream = new MemoryStream(pe);
|
|
|
|
// Act
|
|
var result = PeReader.Parse(stream, "test.exe");
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result!.Identity.Should().NotBeNull();
|
|
result.Identity.Is64Bit.Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_InvalidStream_ReturnsNull()
|
|
{
|
|
// Arrange
|
|
var invalidData = new byte[] { 0x00, 0x01, 0x02, 0x03 };
|
|
using var stream = new MemoryStream(invalidData);
|
|
|
|
// Act
|
|
var result = PeReader.Parse(stream, "invalid.exe");
|
|
|
|
// Assert
|
|
result.Should().BeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_ThrowsOnNullStream()
|
|
{
|
|
// Act & Assert
|
|
var action = () => PeReader.Parse(null!, "test.exe");
|
|
action.Should().Throw<ArgumentNullException>();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Machine Architecture
|
|
|
|
[Theory]
|
|
[InlineData(PeMachine.I386, "x86", false)]
|
|
[InlineData(PeMachine.Amd64, "x86_64", true)]
|
|
[InlineData(PeMachine.Arm64, "arm64", true)]
|
|
public void TryExtractIdentity_MachineTypes_MapCorrectly(PeMachine machine, string expectedArch, bool is64Bit)
|
|
{
|
|
// Arrange
|
|
var pe = new PeBuilder()
|
|
.Is64Bit(is64Bit)
|
|
.WithMachine(machine)
|
|
.Build();
|
|
|
|
// Act
|
|
var result = PeReader.TryExtractIdentity(pe, out var identity);
|
|
|
|
// Assert
|
|
result.Should().BeTrue();
|
|
identity.Should().NotBeNull();
|
|
identity!.Machine.Should().Be(expectedArch);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Exports
|
|
|
|
[Fact]
|
|
public void TryExtractIdentity_NoExports_ReturnsEmptyList()
|
|
{
|
|
// Arrange - standard console app has no exports
|
|
var pe = PeBuilder.Console64().Build();
|
|
|
|
// Act
|
|
var result = PeReader.TryExtractIdentity(pe, out var identity);
|
|
|
|
// Assert
|
|
result.Should().BeTrue();
|
|
identity.Should().NotBeNull();
|
|
identity!.Exports.Should().BeEmpty();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Compiler Hints (Rich Header)
|
|
|
|
[Fact]
|
|
public void TryExtractIdentity_NoRichHeader_ReturnsEmptyHints()
|
|
{
|
|
// Arrange - builder-generated PEs don't have rich header
|
|
var pe = PeBuilder.Console64().Build();
|
|
|
|
// Act
|
|
var result = PeReader.TryExtractIdentity(pe, out var identity);
|
|
|
|
// Assert
|
|
result.Should().BeTrue();
|
|
identity.Should().NotBeNull();
|
|
identity!.CompilerHints.Should().BeEmpty();
|
|
identity.RichHeaderHash.Should().BeNull();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region CodeView Debug Info
|
|
|
|
[Fact]
|
|
public void TryExtractIdentity_NoDebugDirectory_ReturnsNullCodeView()
|
|
{
|
|
// Arrange - builder-generated PEs don't have debug directory
|
|
var pe = PeBuilder.Console64().Build();
|
|
|
|
// Act
|
|
var result = PeReader.TryExtractIdentity(pe, out var identity);
|
|
|
|
// Assert
|
|
result.Should().BeTrue();
|
|
identity.Should().NotBeNull();
|
|
identity!.CodeViewGuid.Should().BeNull();
|
|
identity.CodeViewAge.Should().BeNull();
|
|
identity.PdbPath.Should().BeNull();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Version Resources
|
|
|
|
[Fact]
|
|
public void TryExtractIdentity_NoVersionResource_ReturnsNullVersions()
|
|
{
|
|
// Arrange - builder-generated PEs don't have version resources
|
|
var pe = PeBuilder.Console64().Build();
|
|
|
|
// Act
|
|
var result = PeReader.TryExtractIdentity(pe, out var identity);
|
|
|
|
// Assert
|
|
result.Should().BeTrue();
|
|
identity.Should().NotBeNull();
|
|
identity!.ProductVersion.Should().BeNull();
|
|
identity.FileVersion.Should().BeNull();
|
|
identity.CompanyName.Should().BeNull();
|
|
identity.ProductName.Should().BeNull();
|
|
identity.OriginalFilename.Should().BeNull();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Determinism
|
|
|
|
[Fact]
|
|
public void TryExtractIdentity_SameInput_ReturnsSameOutput()
|
|
{
|
|
// Arrange
|
|
var pe = PeBuilder.Console64().Build();
|
|
|
|
// Act
|
|
PeReader.TryExtractIdentity(pe, out var identity1);
|
|
PeReader.TryExtractIdentity(pe, out var identity2);
|
|
|
|
// Assert
|
|
identity1.Should().BeEquivalentTo(identity2);
|
|
}
|
|
|
|
[Fact]
|
|
public void TryExtractIdentity_DifferentInputs_ReturnsDifferentOutput()
|
|
{
|
|
// Arrange
|
|
var pe64 = PeBuilder.Console64().Build();
|
|
var pe32 = new PeBuilder().Is64Bit(false).Build();
|
|
|
|
// Act
|
|
PeReader.TryExtractIdentity(pe64, out var identity64);
|
|
PeReader.TryExtractIdentity(pe32, out var identity32);
|
|
|
|
// Assert
|
|
identity64!.Is64Bit.Should().NotBe(identity32!.Is64Bit);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Edge Cases
|
|
|
|
[Fact]
|
|
public void TryExtractIdentity_InvalidPeOffset_ReturnsFalse()
|
|
{
|
|
// Arrange - Create data with MZ signature but invalid PE offset
|
|
var data = new byte[0x100];
|
|
data[0] = (byte)'M';
|
|
data[1] = (byte)'Z';
|
|
// Set PE offset beyond file bounds
|
|
data[0x3C] = 0xFF;
|
|
data[0x3D] = 0xFF;
|
|
data[0x3E] = 0x00;
|
|
data[0x3F] = 0x00;
|
|
|
|
// Act
|
|
var result = PeReader.TryExtractIdentity(data, out var identity);
|
|
|
|
// Assert
|
|
result.Should().BeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void TryExtractIdentity_MissingPeSignature_ReturnsFalse()
|
|
{
|
|
// Arrange - Create data with MZ but missing PE signature
|
|
var data = new byte[0x100];
|
|
data[0] = (byte)'M';
|
|
data[1] = (byte)'Z';
|
|
data[0x3C] = 0x80; // PE offset at 0x80
|
|
// No PE signature at offset 0x80
|
|
|
|
// Act
|
|
var result = PeReader.TryExtractIdentity(data, out var identity);
|
|
|
|
// Assert
|
|
result.Should().BeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void TryExtractIdentity_InvalidMagic_ReturnsFalse()
|
|
{
|
|
// Arrange - Create data with PE signature but invalid magic
|
|
var data = new byte[0x200];
|
|
data[0] = (byte)'M';
|
|
data[1] = (byte)'Z';
|
|
data[0x3C] = 0x80; // PE offset at 0x80
|
|
|
|
// PE signature
|
|
data[0x80] = (byte)'P';
|
|
data[0x81] = (byte)'E';
|
|
data[0x82] = 0;
|
|
data[0x83] = 0;
|
|
|
|
// Invalid COFF header with size 0
|
|
data[0x80 + 16] = 0; // SizeOfOptionalHeader = 0
|
|
|
|
// Act
|
|
var result = PeReader.TryExtractIdentity(data, out var identity);
|
|
|
|
// Assert
|
|
result.Should().BeFalse();
|
|
}
|
|
|
|
#endregion
|
|
}
|