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; /// /// Unit tests for PeReader full PE parsing including CodeView GUID, Rich header, and version resources. /// 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(); } #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 }