From 76b1cc85b4e11ad6950178ad13b4a0995bb57634 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20=C5=A0vendov=C3=A1?= Date: Thu, 9 Apr 2026 11:45:58 +0200 Subject: [PATCH 01/36] Add unit tests for DwarfAttributeValue and related DWARF types --- .github/03-feature.instructions.md | 17 +- .memory-bank/activeContext.md | 24 + .memory-bank/learnings.md | 6 + .../Dwarf/DwarfAttributeValueTests.cs | 317 +++++++++++++ .../Dwarf/DwarfCompilationUnitFormTests.cs | 425 +++++++++++++++++- 5 files changed, 787 insertions(+), 2 deletions(-) create mode 100644 .memory-bank/activeContext.md create mode 100644 .memory-bank/learnings.md create mode 100644 src/Test.UnitTests.BinaryParsers/Dwarf/DwarfAttributeValueTests.cs diff --git a/.github/03-feature.instructions.md b/.github/03-feature.instructions.md index 75115739..2ba45b4d 100644 --- a/.github/03-feature.instructions.md +++ b/.github/03-feature.instructions.md @@ -4,6 +4,21 @@ applyTo: "**" ## Feature Instructions - + Add unit tests to binskim class: +DwarfAttributeValue +DwarfCommonInformationEntry +DwarfCompilationUnit +DwarfFrameDescriptionEntry +DwarfLineNumberProgram +DwarfMemoryReader +DwarfSymbol +DwarfProvider +ElfBinary +Nice to have +guardian integration tests +guardian pipeline - run guardian with binskim on 1ESPT +Binskim Integration tests +pester tests (powershell) +Get rid of Verify Methods ## Testing Configuration diff --git a/.memory-bank/activeContext.md b/.memory-bank/activeContext.md new file mode 100644 index 00000000..2cfa8188 --- /dev/null +++ b/.memory-bank/activeContext.md @@ -0,0 +1,24 @@ +# Active Context + +## Current Work Focus +Design and plan unit tests for DWARF-related types, starting with DwarfAttributeValue and including DWARF4/DWARF5-specific behaviors. + +## Recent Changes +- ✅ Defined detailed unit test coverage goals for DwarfAttributeValue (all enum values, accessors, equality, hash code, and ToString). +- ✅ Identified DWARF4 and DWARF5 form mappings in DwarfCompilationUnit that feed into DwarfAttributeValue. + +## Active Decisions +- ✅ Full enum coverage for DwarfAttributeValueType is desired, including currently less-used values like Invalid, ResolvedReference, and Loclistx. +- ✅ Tests should pin current behavior even when it may be surprising (e.g., equality/hash interactions for unhandled enum types), so future changes are explicit. + +## Next Steps +1. ✅ Create a new DwarfAttributeValue-focused unit test class in the BinaryParsers unit test project, following existing naming and structure conventions. +2. ✅ Add tests for each accessor (Address, Block, Constant, String, Flag, Reference, ExpressionLocation, SecOffset) covering “happy path” usage. +3. ✅ Add Constant decoding tests for byte[] lengths 1/2/4/8, unsupported lengths (throw NotImplementedException), and direct ulong Value. +4. ✅ Add equality/hash code tests covering null semantics, type mismatches, handled enum types, and unhandled enum types (Invalid, ResolvedReference, Loclistx). +5. ✅ Add DWARF4-oriented tests that verify DwarfCompilationUnit maps classic forms (Data1/2/4/8, SData, UData, Address, Block*, String/Strp, Flag*, Ref*) into the expected DwarfAttributeValue Type/Value combinations. +6. ✅ Add DWARF5-oriented tests that verify newer forms (LineStrp, StrpSup, Strx*/GNUStrIndex, Addrx*/GNUAddrIndex, Rnglistx, Loclistx, RefSup*, RefSig8) are mapped to the correct Type/Value/Offset in DwarfAttributeValue. +7. ✅ Add a couple of small end-to-end DWARF4 and DWARF5 synthetic units that exercise representative attributes and assert on the resulting DwarfAttributeValue objects. + +## Current State +All planned DWARF-related unit tests, including end-to-end DWARF4 and DWARF5 synthetic units, are implemented, and the BinaryParsers unit tests are currently passing. diff --git a/.memory-bank/learnings.md b/.memory-bank/learnings.md new file mode 100644 index 00000000..9679edb1 --- /dev/null +++ b/.memory-bank/learnings.md @@ -0,0 +1,6 @@ +# Learnings + +- Initialized learnings file; no project-specific learnings recorded yet. +- DwarfAttributeValue represents DWARF attribute values with a Type enum (DwarfAttributeValueType) and a boxed Value, plus an optional Offset used for deferred resolution. +- Equality in DwarfAttributeValue is type-sensitive and uses special handling for some enum values (Address, Constant, Reference, SecOffset, Block, ExpressionLocation, Flag, String); other enum values currently fall through to a default “equal” path regardless of Value, which is important to keep in mind when adding tests. +- DwarfCompilationUnit maps DWARF4 forms (Data*, Address, Block*, String/Strp, Flag*, Ref*, ExpressionLocation, SecOffset, etc.) and DWARF5/extended forms (LineStrp, StrpSup, Strx*/GNUStrIndex, Addrx*/GNUAddrIndex, Rnglistx, Loclistx, RefSup*, RefSig8) into DwarfAttributeValue instances by setting Type, Value, and/or Offset according to the DWARF spec and LLVM’s DWARFFormValue behavior. diff --git a/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfAttributeValueTests.cs b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfAttributeValueTests.cs new file mode 100644 index 00000000..44cd2bf0 --- /dev/null +++ b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfAttributeValueTests.cs @@ -0,0 +1,317 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; + +using FluentAssertions; + +using Xunit; + +namespace Microsoft.CodeAnalysis.BinaryParsers.Dwarf +{ + /// + /// Unit tests for . + /// + public class DwarfAttributeValueTests + { + [Fact] + public void Address_ReturnsUnderlyingUlong() + { + var value = new DwarfAttributeValue + { + Type = DwarfAttributeValueType.Address, + Value = 0x12345678UL, + }; + + value.Address.Should().Be(0x12345678UL); + } + + [Fact] + public void Block_ReturnsUnderlyingByteArray() + { + byte[] bytes = { 0x01, 0x02, 0x03 }; + + var value = new DwarfAttributeValue + { + Type = DwarfAttributeValueType.Block, + Value = bytes, + }; + + value.Block.Should().Equal(0x01, 0x02, 0x03); + } + + [Fact] + public void Constant_ReturnsUnderlyingUlong_WhenValueIsUlong() + { + var value = new DwarfAttributeValue + { + Type = DwarfAttributeValueType.Constant, + Value = 42UL, + }; + + value.Constant.Should().Be(42UL); + } + + [Theory] + [InlineData(new byte[] { 0x2A }, 0x2AUL)] // 1 byte + [InlineData(new byte[] { 0x34, 0x12 }, 0x1234UL)] // 2 bytes (LE) + [InlineData(new byte[] { 0x78, 0x56, 0x34, 0x12 }, 0x12345678UL)] // 4 bytes (LE) + [InlineData(new byte[] { 0xEF, 0xCD, 0xAB, 0x90, 0x78, 0x56, 0x34, 0x12 }, 0x1234567890ABCDEFUL)] // 8 bytes (LE) + public void Constant_DecodesSupportedByteArrayLengths(byte[] bytes, ulong expected) + { + var value = new DwarfAttributeValue + { + Type = DwarfAttributeValueType.Constant, + Value = bytes, + }; + + value.Constant.Should().Be(expected); + } + + [Fact] + public void Constant_ThrowsForUnsupportedByteArrayLength() + { + var value = new DwarfAttributeValue + { + Type = DwarfAttributeValueType.Constant, + Value = new byte[] { 0x01, 0x02, 0x03 }, // 3 bytes → not implemented + }; + + Action act = () => _ = value.Constant; + + act.Should().Throw(); + } + + [Fact] + public void String_ReturnsUnderlyingString() + { + var value = new DwarfAttributeValue + { + Type = DwarfAttributeValueType.String, + Value = "hello", + }; + + value.String.Should().Be("hello"); + } + + [Fact] + public void Flag_ReturnsUnderlyingBool() + { + var value = new DwarfAttributeValue + { + Type = DwarfAttributeValueType.Flag, + Value = true, + }; + + value.Flag.Should().BeTrue(); + } + + [Fact] + public void Reference_ReturnsUnderlyingDwarfSymbol() + { + var symbol = new DwarfSymbol(); + + var value = new DwarfAttributeValue + { + Type = DwarfAttributeValueType.ResolvedReference, + Value = symbol, + }; + + value.Reference.Should().BeSameAs(symbol); + } + + [Fact] + public void ExpressionLocation_ReturnsUnderlyingByteArray() + { + byte[] bytes = { 0xAA, 0xBB }; + + var value = new DwarfAttributeValue + { + Type = DwarfAttributeValueType.ExpressionLocation, + Value = bytes, + }; + + value.ExpressionLocation.Should().Equal(0xAA, 0xBB); + } + + [Fact] + public void SecOffset_ReturnsUnderlyingUlong() + { + var value = new DwarfAttributeValue + { + Type = DwarfAttributeValueType.SecOffset, + Value = 0xCAFEBABEUL, + }; + + value.SecOffset.Should().Be(0xCAFEBABEUL); + } + + // ---- Equality and hash code semantics ---- + + [Fact] + public void Equals_ReturnsTrue_WhenBothNull() + { + DwarfAttributeValue left = null; + DwarfAttributeValue right = null; + + (left == right).Should().BeTrue(); + (left != right).Should().BeFalse(); + } + + [Fact] + public void Equals_ReturnsFalse_WhenOnlyOneNull() + { + var value = new DwarfAttributeValue + { + Type = DwarfAttributeValueType.Constant, + Value = 1UL, + }; + + (value == null).Should().BeFalse(); + (null == value).Should().BeFalse(); + (value != null).Should().BeTrue(); + (null != value).Should().BeTrue(); + } + + [Fact] + public void Equals_ReturnsFalse_WhenTypesDiffer() + { + var value1 = new DwarfAttributeValue + { + Type = DwarfAttributeValueType.Constant, + Value = 1UL, + }; + + var value2 = new DwarfAttributeValue + { + Type = DwarfAttributeValueType.Address, + Value = 1UL, + }; + + (value1 == value2).Should().BeFalse(); + (value1 != value2).Should().BeTrue(); + } + + [Fact] + public void Equals_HandledScalarTypes_UseUnderlyingUlong() + { + var constant1 = new DwarfAttributeValue { Type = DwarfAttributeValueType.Constant, Value = 10UL }; + var constant2 = new DwarfAttributeValue { Type = DwarfAttributeValueType.Constant, Value = 10UL }; + var constant3 = new DwarfAttributeValue { Type = DwarfAttributeValueType.Constant, Value = 11UL }; + + (constant1 == constant2).Should().BeTrue(); + (constant1 == constant3).Should().BeFalse(); + + var address1 = new DwarfAttributeValue { Type = DwarfAttributeValueType.Address, Value = 0x1000UL }; + var address2 = new DwarfAttributeValue { Type = DwarfAttributeValueType.Address, Value = 0x1000UL }; + var address3 = new DwarfAttributeValue { Type = DwarfAttributeValueType.Address, Value = 0x2000UL }; + + (address1 == address2).Should().BeTrue(); + (address1 == address3).Should().BeFalse(); + + var secOffset1 = new DwarfAttributeValue { Type = DwarfAttributeValueType.SecOffset, Value = 5UL }; + var secOffset2 = new DwarfAttributeValue { Type = DwarfAttributeValueType.SecOffset, Value = 5UL }; + var secOffset3 = new DwarfAttributeValue { Type = DwarfAttributeValueType.SecOffset, Value = 6UL }; + + (secOffset1 == secOffset2).Should().BeTrue(); + (secOffset1 == secOffset3).Should().BeFalse(); + + var reference1 = new DwarfAttributeValue { Type = DwarfAttributeValueType.Reference, Value = 123UL }; + var reference2 = new DwarfAttributeValue { Type = DwarfAttributeValueType.Reference, Value = 123UL }; + var reference3 = new DwarfAttributeValue { Type = DwarfAttributeValueType.Reference, Value = 124UL }; + + (reference1 == reference2).Should().BeTrue(); + (reference1 == reference3).Should().BeFalse(); + } + + [Fact] + public void Equals_HandledArrayAndStringTypes_UseSequenceAndStringComparison() + { + var block1 = new DwarfAttributeValue { Type = DwarfAttributeValueType.Block, Value = new byte[] { 0x01, 0x02 } }; + var block2 = new DwarfAttributeValue { Type = DwarfAttributeValueType.Block, Value = new byte[] { 0x01, 0x02 } }; + var block3 = new DwarfAttributeValue { Type = DwarfAttributeValueType.Block, Value = new byte[] { 0x01, 0x03 } }; + + (block1 == block2).Should().BeTrue(); + (block1 == block3).Should().BeFalse(); + + var expr1 = new DwarfAttributeValue { Type = DwarfAttributeValueType.ExpressionLocation, Value = new byte[] { 0xAA } }; + var expr2 = new DwarfAttributeValue { Type = DwarfAttributeValueType.ExpressionLocation, Value = new byte[] { 0xAA } }; + var expr3 = new DwarfAttributeValue { Type = DwarfAttributeValueType.ExpressionLocation, Value = new byte[] { 0xBB } }; + + (expr1 == expr2).Should().BeTrue(); + (expr1 == expr3).Should().BeFalse(); + + var flag1 = new DwarfAttributeValue { Type = DwarfAttributeValueType.Flag, Value = true }; + var flag2 = new DwarfAttributeValue { Type = DwarfAttributeValueType.Flag, Value = true }; + var flag3 = new DwarfAttributeValue { Type = DwarfAttributeValueType.Flag, Value = false }; + + (flag1 == flag2).Should().BeTrue(); + (flag1 == flag3).Should().BeFalse(); + + var string1 = new DwarfAttributeValue { Type = DwarfAttributeValueType.String, Value = "name" }; + var string2 = new DwarfAttributeValue { Type = DwarfAttributeValueType.String, Value = new string("name".ToCharArray()) }; + var string3 = new DwarfAttributeValue { Type = DwarfAttributeValueType.String, Value = "other" }; + + (string1 == string2).Should().BeTrue(); + (string1 == string3).Should().BeFalse(); + } + + [Fact] + public void Equals_UnhandledTypes_IgnoreValueAndTreatSameTypeAsEqual() + { + var invalid1 = new DwarfAttributeValue { Type = DwarfAttributeValueType.Invalid, Value = 1UL }; + var invalid2 = new DwarfAttributeValue { Type = DwarfAttributeValueType.Invalid, Value = 2UL }; + + (invalid1 == invalid2).Should().BeTrue(); + + var resolved1 = new DwarfAttributeValue { Type = DwarfAttributeValueType.ResolvedReference, Value = new DwarfSymbol() }; + var resolved2 = new DwarfAttributeValue { Type = DwarfAttributeValueType.ResolvedReference, Value = new DwarfSymbol() }; + + (resolved1 == resolved2).Should().BeTrue(); + + var loclistx1 = new DwarfAttributeValue { Type = DwarfAttributeValueType.Loclistx, Value = 10UL }; + var loclistx2 = new DwarfAttributeValue { Type = DwarfAttributeValueType.Loclistx, Value = 20UL }; + + (loclistx1 == loclistx2).Should().BeTrue(); + } + + [Fact] + public void GetHashCode_MatchesForEqualValues_OnHandledType() + { + var v1 = new DwarfAttributeValue + { + Type = DwarfAttributeValueType.Constant, + Value = 123UL, + }; + + var v2 = new DwarfAttributeValue + { + Type = DwarfAttributeValueType.Constant, + Value = 123UL, + }; + + v1.Equals(v2).Should().BeTrue(); + v1.GetHashCode().Should().Be(v2.GetHashCode()); + } + + [Fact] + public void GetHashCode_MayDifferForEqualValues_OnUnhandledType() + { + var v1 = new DwarfAttributeValue + { + Type = DwarfAttributeValueType.Loclistx, + Value = 1UL, + }; + + var v2 = new DwarfAttributeValue + { + Type = DwarfAttributeValueType.Loclistx, + Value = 2UL, + }; + + // Current implementation considers these equal but uses Value in the hash code. + v1.Equals(v2).Should().BeTrue(); + v1.GetHashCode().Should().NotBe(v2.GetHashCode()); + } + } +} diff --git a/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfCompilationUnitFormTests.cs b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfCompilationUnitFormTests.cs index 13fa0047..b3a929b7 100644 --- a/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfCompilationUnitFormTests.cs +++ b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfCompilationUnitFormTests.cs @@ -168,11 +168,19 @@ private static byte[] BuildAbbrevMultipleAttributes(DwarfTag tag, params (DwarfA /// Parses a DWARF compilation unit from crafted binary data and returns the first symbol. /// private static DwarfSymbol ParseSingleSymbol(byte[] debugInfoData, byte[] debugAbbrevData, byte[] debugStrData = null) + { + return ParseSingleSymbolWithLineStrings(debugInfoData, debugAbbrevData, debugStrData, new byte[] { 0x00 }); + } + + /// + /// Parses a DWARF compilation unit with explicit .debug_str and .debug_line_str data. + /// + private static DwarfSymbol ParseSingleSymbolWithLineStrings(byte[] debugInfoData, byte[] debugAbbrevData, byte[] debugStrData, byte[] debugLineStrData) { using var debugData = new DwarfMemoryReader(debugInfoData); using var debugAbbrev = new DwarfMemoryReader(debugAbbrevData); using var debugStrings = new DwarfMemoryReader(debugStrData ?? new byte[] { 0x00 }); - using var debugLineStrings = new DwarfMemoryReader(new byte[] { 0x00 }); + using var debugLineStrings = new DwarfMemoryReader(debugLineStrData ?? new byte[] { 0x00 }); var stub = new StubDwarfBinary(); var stringOffsets = new List(); @@ -408,6 +416,332 @@ public void ReadData_GNUAddrIndex_MultiByte_FollowedByData1() ((ulong)symbol.Attributes[DwarfAttribute.ByteSize].Value).Should().Be(0x99); } + // ---- DWARF4 classic forms: Data*, SData, UData, Address, Block*, String/Strp, Flag*, Ref* ---- + + [Fact] + public void ReadData_Dwarf4_DataForms_MapToConstant() + { + // Data1 = 0x11, Data2 = 0x2222, Data4 = 0x33333333, Data8 = 0x4444444444444444 + byte[] dieData = new byte[] + { + 0x01, // abbreviation code + 0x11, // Data1 + 0x22, 0x22, // Data2 + 0x33, 0x33, 0x33, 0x33, // Data4 + 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, // Data8 + }; + + byte[] header = BuildDwarf4Header((uint)dieData.Length); + byte[] debugInfo = header.Concat(dieData).ToArray(); + byte[] abbrev = BuildAbbrevMultipleAttributes(DwarfTag.Variable, + (DwarfAttribute.Location, DwarfFormat.Data1), + (DwarfAttribute.ByteSize, DwarfFormat.Data2), + (DwarfAttribute.DeclFile, DwarfFormat.Data4), + (DwarfAttribute.DeclLine, DwarfFormat.Data8)); + + DwarfSymbol symbol = ParseSingleSymbol(debugInfo, abbrev); + + symbol.Attributes[DwarfAttribute.Location].Type.Should().Be(DwarfAttributeValueType.Constant); + ((ulong)symbol.Attributes[DwarfAttribute.Location].Value).Should().Be(0x11); + + symbol.Attributes[DwarfAttribute.ByteSize].Type.Should().Be(DwarfAttributeValueType.Constant); + ((ulong)symbol.Attributes[DwarfAttribute.ByteSize].Value).Should().Be(0x2222); + + symbol.Attributes[DwarfAttribute.DeclFile].Type.Should().Be(DwarfAttributeValueType.Constant); + ((ulong)symbol.Attributes[DwarfAttribute.DeclFile].Value).Should().Be(0x33333333); + + symbol.Attributes[DwarfAttribute.DeclLine].Type.Should().Be(DwarfAttributeValueType.Constant); + ((ulong)symbol.Attributes[DwarfAttribute.DeclLine].Value).Should().Be(0x4444444444444444UL); + } + + [Fact] + public void ReadData_Dwarf4_SDataAndUData_MapToConstant() + { + // SData = -2, UData = 300 + byte[] sdata = EncodeULEB128(0x7E); // SLEB128(-2) payload read via SData + byte[] udata = EncodeULEB128(300); // ULEB128(300) + + byte[] dieData = new byte[] { 0x01 } + .Concat(sdata) // SData + .Concat(udata) // UData + .ToArray(); + + byte[] header = BuildDwarf4Header((uint)dieData.Length); + byte[] debugInfo = header.Concat(dieData).ToArray(); + byte[] abbrev = BuildAbbrevMultipleAttributes(DwarfTag.Variable, + (DwarfAttribute.LowPc, DwarfFormat.SData), + (DwarfAttribute.HighPc, DwarfFormat.UData)); + + DwarfSymbol symbol = ParseSingleSymbol(debugInfo, abbrev); + + symbol.Attributes[DwarfAttribute.LowPc].Type.Should().Be(DwarfAttributeValueType.Constant); + // Current implementation stores SData as a 32-bit signed value widened to ulong. + ((ulong)symbol.Attributes[DwarfAttribute.LowPc].Value).Should().Be(4294967294UL); + + symbol.Attributes[DwarfAttribute.HighPc].Type.Should().Be(DwarfAttributeValueType.Constant); + ((ulong)symbol.Attributes[DwarfAttribute.HighPc].Value).Should().Be(300UL); + } + + [Fact] + public void ReadData_Dwarf4_AddressAndRefForms_MapToAddressAndReference() + { + // Address: 8-byte value, Ref4: 4-byte offset from CU begin + byte[] dieData = new byte[] + { + 0x01, + // Address (8 bytes LE) + 0x78, 0x56, 0x34, 0x12, 0xEF, 0xCD, 0xAB, 0x90, + // Ref4 (4 bytes LE) = 0x20 from CU begin (we don't need a real target DIE for this test) + 0x20, 0x00, 0x00, 0x00, + }; + + byte[] header = BuildDwarf4Header((uint)dieData.Length, addressSize: 8); + byte[] debugInfo = header.Concat(dieData).ToArray(); + byte[] abbrev = BuildAbbrevMultipleAttributes(DwarfTag.Variable, + (DwarfAttribute.LowPc, DwarfFormat.Address), + (DwarfAttribute.Type, DwarfFormat.Ref4)); + + DwarfSymbol symbol = ParseSingleSymbol(debugInfo, abbrev); + + symbol.Attributes[DwarfAttribute.LowPc].Type.Should().Be(DwarfAttributeValueType.Address); + ((ulong)symbol.Attributes[DwarfAttribute.LowPc].Value).Should().Be(0x90ABCDEF12345678UL); + + symbol.Attributes[DwarfAttribute.Type].Type.Should().Be(DwarfAttributeValueType.Reference); + ((ulong)symbol.Attributes[DwarfAttribute.Type].Value).Should().BeGreaterThan(0UL); // offset from CU begin + } + + [Fact] + public void ReadData_Dwarf4_BlockAndStringForms_MapToBlockAndString() + { + // Block: length-prefixed via ULEB128; String: null-terminated inline; Strp: offset into .debug_str + byte[] blockPayload = new byte[] { 0xAA, 0xBB }; + byte[] blockLen = EncodeULEB128((ulong)blockPayload.Length); + byte[] inlineString = new byte[] { (byte)'H', (byte)'i', 0x00 }; + + byte[] dieData = new byte[] { 0x01 } + .Concat(blockLen) // Block length + .Concat(blockPayload) // Block bytes + .Concat(inlineString) // String + .Concat(new byte[] { 0x02, 0x00 }) // Strp: offset 2 into debug_str + .ToArray(); + + // .debug_str: two bytes of padding, then "World\0" + byte[] debugStr = new byte[] { 0x00, 0x00, (byte)'W', (byte)'o', (byte)'r', (byte)'l', (byte)'d', 0x00 }; + + byte[] header = BuildDwarf4Header((uint)dieData.Length); + byte[] debugInfo = header.Concat(dieData).ToArray(); + byte[] abbrev = BuildAbbrevMultipleAttributes(DwarfTag.Variable, + (DwarfAttribute.Location, DwarfFormat.Block), + (DwarfAttribute.Name, DwarfFormat.String), + (DwarfAttribute.CompDir, DwarfFormat.Strp)); + + DwarfSymbol symbol = ParseSingleSymbol(debugInfo, abbrev, debugStr); + + symbol.Attributes[DwarfAttribute.Location].Type.Should().Be(DwarfAttributeValueType.Block); + symbol.Attributes[DwarfAttribute.Location].Block.Should().Equal(0xAA, 0xBB); + + symbol.Attributes[DwarfAttribute.Name].Type.Should().Be(DwarfAttributeValueType.String); + symbol.Attributes[DwarfAttribute.Name].String.Should().Be("Hi"); + + symbol.Attributes[DwarfAttribute.CompDir].Type.Should().Be(DwarfAttributeValueType.String); + symbol.Attributes[DwarfAttribute.CompDir].String.Should().Be("World"); + } + + [Fact] + public void ReadData_Dwarf4_FlagForms_MapToFlag() + { + byte[] dieData = new byte[] + { + 0x01, + 0x01, // Flag = true + }; + + byte[] header = BuildDwarf4Header((uint)dieData.Length); + byte[] debugInfo = header.Concat(dieData).ToArray(); + byte[] abbrev = BuildAbbrevMultipleAttributes(DwarfTag.Variable, + (DwarfAttribute.External, DwarfFormat.Flag), + (DwarfAttribute.Declaration, DwarfFormat.FlagPresent)); + + DwarfSymbol symbol = ParseSingleSymbol(debugInfo, abbrev); + + symbol.Attributes[DwarfAttribute.External].Type.Should().Be(DwarfAttributeValueType.Flag); + symbol.Attributes[DwarfAttribute.External].Flag.Should().BeTrue(); + + symbol.Attributes[DwarfAttribute.Declaration].Type.Should().Be(DwarfAttributeValueType.Flag); + symbol.Attributes[DwarfAttribute.Declaration].Flag.Should().BeTrue(); + } + + // ---- DWARF5 newer forms: LineStrp, StrpSup, Strx/Strx1/2/4, GNUStrIndex, Addrx, RefSup*, RefSig8 ---- + + [Fact] + public void ReadData_LineStrp_ResolvesFromDebugLineStrings() + { + // .debug_line_str: two bytes padding, then "Line\0" + byte[] debugLineStr = new byte[] { 0x00, 0x00, (byte)'L', (byte)'i', (byte)'n', (byte)'e', 0x00 }; + + // DIE: abbrev code 1, then 32-bit offset 2 into .debug_line_str + byte[] dieData = new byte[] + { + 0x01, + 0x02, 0x00, 0x00, 0x00, + }; + + byte[] header = BuildDwarf5Header((uint)dieData.Length); + byte[] debugInfo = header.Concat(dieData).ToArray(); + byte[] abbrev = BuildAbbrevSingleAttribute(DwarfTag.Variable, DwarfAttribute.Name, DwarfFormat.LineStrp); + + DwarfSymbol symbol = ParseSingleSymbolWithLineStrings(debugInfo, abbrev, new byte[] { 0x00 }, debugLineStr); + + symbol.Attributes[DwarfAttribute.Name].Type.Should().Be(DwarfAttributeValueType.String); + symbol.Attributes[DwarfAttribute.Name].String.Should().Be("Line"); + } + + [Fact] + public void ReadData_StrpSup_SetsOffsetAndLeavesValueNull() + { + byte[] dieData = new byte[] + { + 0x01, + 0x04, 0x03, 0x02, 0x01, // offset = 0x01020304 + }; + + byte[] header = BuildDwarf5Header((uint)dieData.Length); + byte[] debugInfo = header.Concat(dieData).ToArray(); + byte[] abbrev = BuildAbbrevSingleAttribute(DwarfTag.Variable, DwarfAttribute.Name, DwarfFormat.StrpSup); + + DwarfSymbol symbol = ParseSingleSymbol(debugInfo, abbrev); + + symbol.Attributes[DwarfAttribute.Name].Type.Should().Be(DwarfAttributeValueType.String); + symbol.Attributes[DwarfAttribute.Name].Offset.Should().Be(0x01020304UL); + symbol.Attributes[DwarfAttribute.Name].Value.Should().BeNull(); + } + + [Fact] + public void ReadData_StrxAndGnuStrIndex_SetStringOffsets() + { + // ULEB128(5) then ULEB128(10) + byte[] dieData = new byte[] + { + 0x01, + 0x05, // Strx index + 0x0A, // GNUStrIndex index + }; + + byte[] header = BuildDwarf5Header((uint)dieData.Length); + byte[] debugInfo = header.Concat(dieData).ToArray(); + byte[] abbrev = BuildAbbrevMultipleAttributes(DwarfTag.Variable, + (DwarfAttribute.Name, DwarfFormat.Strx), + (DwarfAttribute.CompDir, DwarfFormat.GNUStrIndex)); + + DwarfSymbol symbol = ParseSingleSymbol(debugInfo, abbrev); + + symbol.Attributes[DwarfAttribute.Name].Type.Should().Be(DwarfAttributeValueType.String); + symbol.Attributes[DwarfAttribute.Name].Offset.Should().Be(5UL); + + symbol.Attributes[DwarfAttribute.CompDir].Type.Should().Be(DwarfAttributeValueType.String); + symbol.Attributes[DwarfAttribute.CompDir].Offset.Should().Be(10UL); + } + + [Fact] + public void ReadData_StrxFixedWidthForms_SetExpectedOffsets() + { + byte[] dieData = new byte[] + { + 0x01, + 0x0A, // Strx1 = 10 + 0x22, 0x11, // Strx2 = 0x1122 + 0x33, 0x22, 0x11, // Strx3 = 0x112233 + 0x44, 0x33, 0x22, 0x11, // Strx4 = 0x11223344 + }; + + byte[] header = BuildDwarf5Header((uint)dieData.Length); + byte[] debugInfo = header.Concat(dieData).ToArray(); + byte[] abbrev = BuildAbbrevMultipleAttributes(DwarfTag.Variable, + (DwarfAttribute.Name, DwarfFormat.Strx1), + (DwarfAttribute.CompDir, DwarfFormat.Strx2), + (DwarfAttribute.DeclFile, DwarfFormat.Strx3), + (DwarfAttribute.DeclLine, DwarfFormat.Strx4)); + + DwarfSymbol symbol = ParseSingleSymbol(debugInfo, abbrev); + + symbol.Attributes[DwarfAttribute.Name].Offset.Should().Be(10UL); + symbol.Attributes[DwarfAttribute.CompDir].Offset.Should().Be(0x1122UL); + symbol.Attributes[DwarfAttribute.DeclFile].Offset.Should().Be(0x112233UL); + symbol.Attributes[DwarfAttribute.DeclLine].Offset.Should().Be(0x11223344UL); + } + + [Fact] + public void ReadData_Addrx_MapsToAddress() + { + // ULEB128(7) index; beginPosition offset is added internally, but we only + // need to verify that the resulting attribute type is Address. + byte[] dieData = new byte[] + { + 0x01, + 0x07, + }; + + byte[] header = BuildDwarf5Header((uint)dieData.Length); + byte[] debugInfo = header.Concat(dieData).ToArray(); + byte[] abbrev = BuildAbbrevSingleAttribute(DwarfTag.Variable, DwarfAttribute.LowPc, DwarfFormat.Addrx); + + DwarfSymbol symbol = ParseSingleSymbol(debugInfo, abbrev); + + symbol.Attributes[DwarfAttribute.LowPc].Type.Should().Be(DwarfAttributeValueType.Address); + } + + [Fact] + public void ReadData_RefSupForms_SetReferenceOffsets() + { + byte[] dieData = new byte[] + { + 0x01, + 0x10, 0x00, 0x00, 0x00, // RefSup4 = 16 + 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // RefSup8 = 32 + }; + + byte[] header = BuildDwarf5Header((uint)dieData.Length); + byte[] debugInfo = header.Concat(dieData).ToArray(); + byte[] abbrev = BuildAbbrevMultipleAttributes(DwarfTag.Variable, + (DwarfAttribute.Specification, DwarfFormat.RefSup4), + (DwarfAttribute.Type, DwarfFormat.RefSup8)); + + DwarfSymbol symbol = ParseSingleSymbol(debugInfo, abbrev); + + symbol.Attributes[DwarfAttribute.Specification].Type.Should().Be(DwarfAttributeValueType.Reference); + symbol.Attributes[DwarfAttribute.Specification].Offset.Should().Be(16UL); + symbol.Attributes[DwarfAttribute.Specification].Value.Should().BeNull(); + + symbol.Attributes[DwarfAttribute.Type].Type.Should().Be(DwarfAttributeValueType.Reference); + symbol.Attributes[DwarfAttribute.Type].Offset.Should().Be(32UL); + symbol.Attributes[DwarfAttribute.Type].Value.Should().BeNull(); + } + + [Fact] + public void ReadData_RefSig8_SkipsEightBytesAndMarksInvalid() + { + byte[] dieData = new byte[] + { + 0x01, + // RefSig8 payload (8 bytes) + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, + // Following Data1 attribute + 0x42, + }; + + byte[] header = BuildDwarf5Header((uint)dieData.Length); + byte[] debugInfo = header.Concat(dieData).ToArray(); + byte[] abbrev = BuildAbbrevMultipleAttributes(DwarfTag.Variable, + (DwarfAttribute.Type, DwarfFormat.RefSig8), + (DwarfAttribute.ByteSize, DwarfFormat.Data1)); + + DwarfSymbol symbol = ParseSingleSymbol(debugInfo, abbrev); + + symbol.Attributes[DwarfAttribute.Type].Type.Should().Be(DwarfAttributeValueType.Invalid); + symbol.Attributes[DwarfAttribute.ByteSize].Type.Should().Be(DwarfAttributeValueType.Constant); + ((ulong)symbol.Attributes[DwarfAttribute.ByteSize].Value).Should().Be(0x42UL); + } + // ---- DWARF5 header parsing for DW_UT_type ---- [Fact] @@ -488,6 +822,95 @@ public void ReadData_MultipleFormsInSequence_AllConsumeCorrectBytes() ((ulong)symbol.Attributes[DwarfAttribute.DeclFile].Value).Should().Be(0x12345678); } + [Fact] + public void EndToEnd_Dwarf4_VariableSymbol_ParsesRepresentativeAttributes() + { + byte[] nameBytes = new byte[] { (byte)'v', (byte)'a', (byte)'r', 0x00 }; + byte[] addressBytes = new byte[] { 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11 }; // 0x1122334455667788 + + byte[] dieData = new byte[] { 0x01 } + .Concat(nameBytes) // Name: "var" + .Concat(addressBytes) // LowPc: DW_FORM_addr + .Concat(new byte[] { 0x10 })// ByteSize: DW_FORM_data1 = 16 + .Concat(new byte[] { 0x01 })// External: DW_FORM_flag = true + .ToArray(); + + byte[] header = BuildDwarf4Header((uint)dieData.Length); + byte[] debugInfo = header.Concat(dieData).ToArray(); + byte[] abbrev = BuildAbbrevMultipleAttributes(DwarfTag.Variable, + (DwarfAttribute.Name, DwarfFormat.String), + (DwarfAttribute.LowPc, DwarfFormat.Address), + (DwarfAttribute.ByteSize, DwarfFormat.Data1), + (DwarfAttribute.External, DwarfFormat.Flag)); + + DwarfSymbol symbol = ParseSingleSymbol(debugInfo, abbrev); + + symbol.Tag.Should().Be(DwarfTag.Variable); + symbol.Name.Should().Be("var"); + + symbol.Attributes[DwarfAttribute.Name].Type.Should().Be(DwarfAttributeValueType.String); + symbol.Attributes[DwarfAttribute.Name].String.Should().Be("var"); + + symbol.Attributes[DwarfAttribute.LowPc].Type.Should().Be(DwarfAttributeValueType.Address); + symbol.Attributes[DwarfAttribute.LowPc].Address.Should().Be(0x1122334455667788UL); + + symbol.Attributes[DwarfAttribute.ByteSize].Type.Should().Be(DwarfAttributeValueType.Constant); + ((ulong)symbol.Attributes[DwarfAttribute.ByteSize].Value).Should().Be(0x10UL); + + symbol.Attributes[DwarfAttribute.External].Type.Should().Be(DwarfAttributeValueType.Flag); + symbol.Attributes[DwarfAttribute.External].Flag.Should().BeTrue(); + } + + [Fact] + public void EndToEnd_Dwarf5_VariableSymbol_ParsesRepresentativeAttributes() + { + // .debug_line_str: two bytes padding, then "foo\0" + byte[] debugLineStr = new byte[] { 0x00, 0x00, (byte)'f', (byte)'o', (byte)'o', 0x00 }; + + byte[] dieData = new byte[] + { + 0x01, // abbreviation code + 0x02, 0x00, 0x00, 0x00, // Name: LineStrp offset = 2 + 0x03, // CompDir: Strx1 index = 3 + 0x05, // Location: Loclistx = ULEB128(5) + 0x07, // Ranges: Rnglistx = ULEB128(7) + 0x34, 0x12, // ByteSize: Data2 = 0x1234 + }; + + byte[] header = BuildDwarf5Header((uint)dieData.Length); + byte[] debugInfo = header.Concat(dieData).ToArray(); + byte[] abbrev = BuildAbbrevMultipleAttributes(DwarfTag.Variable, + (DwarfAttribute.Name, DwarfFormat.LineStrp), + (DwarfAttribute.CompDir, DwarfFormat.Strx1), + (DwarfAttribute.Location, DwarfFormat.Loclistx), + (DwarfAttribute.Ranges, DwarfFormat.Rnglistx), + (DwarfAttribute.ByteSize, DwarfFormat.Data2)); + + DwarfSymbol symbol = ParseSingleSymbolWithLineStrings( + debugInfo, + abbrev, + new byte[] { 0x00 }, // .debug_str (unused in this test) + debugLineStr); + + symbol.Tag.Should().Be(DwarfTag.Variable); + symbol.Name.Should().Be("foo"); + + symbol.Attributes[DwarfAttribute.Name].Type.Should().Be(DwarfAttributeValueType.String); + symbol.Attributes[DwarfAttribute.Name].String.Should().Be("foo"); + + symbol.Attributes[DwarfAttribute.CompDir].Type.Should().Be(DwarfAttributeValueType.String); + symbol.Attributes[DwarfAttribute.CompDir].Offset.Should().Be(3UL); + + symbol.Attributes[DwarfAttribute.Location].Type.Should().Be(DwarfAttributeValueType.Loclistx); + ((ulong)symbol.Attributes[DwarfAttribute.Location].Value).Should().Be(5UL); + + symbol.Attributes[DwarfAttribute.Ranges].Type.Should().Be(DwarfAttributeValueType.SecOffset); + ((ulong)symbol.Attributes[DwarfAttribute.Ranges].Value).Should().Be(7UL); + + symbol.Attributes[DwarfAttribute.ByteSize].Type.Should().Be(DwarfAttributeValueType.Constant); + ((ulong)symbol.Attributes[DwarfAttribute.ByteSize].Value).Should().Be(0x1234UL); + } + // Abbreviation table terminator handling (DWARF5 spec 7.5.3) [Fact] From cb5a38460b8bf2cf9c516acbed22148cc2a59c8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20=C5=A0vendov=C3=A1?= Date: Mon, 13 Apr 2026 15:13:43 +0200 Subject: [PATCH 02/36] Add unit tests for DwarfCommonInformationEntry to validate parsing behavior --- .../Dwarf/DwarfCommonInformationEntryTests.cs | 296 ++++++++++++++++++ 1 file changed, 296 insertions(+) create mode 100644 src/Test.UnitTests.BinaryParsers/Dwarf/DwarfCommonInformationEntryTests.cs diff --git a/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfCommonInformationEntryTests.cs b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfCommonInformationEntryTests.cs new file mode 100644 index 00000000..0ebe83f5 --- /dev/null +++ b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfCommonInformationEntryTests.cs @@ -0,0 +1,296 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; + +using FluentAssertions; + +using Microsoft.CodeAnalysis.BinaryParsers.Dwarf; + +using Xunit; + +namespace Microsoft.CodeAnalysis.BinaryParsers.Dwarf +{ + /// + /// Unit tests for and its interaction with + /// when parsing .debug_frame style data. + /// + public class DwarfCommonInformationEntryTests + { + [Fact] + public void ParseAll_WithNonEmptyAugmentation_UsesFixedDefaultsAndReadsInstructions() + { + // Layout for a single Common Information Entry (CIE): + // length (4 bytes) + // offset (4 bytes) -- unused for CIE + // second offset = -1 (4 bytes) -- sentinel indicating CIE + // version (1 byte) + // augmentation string "x" + null terminator (2 bytes) + // initial instructions (2 bytes) + + byte[] data = new byte[] + { + // length = 13 bytes following this field (4 + 4 + 1 + 2 + 2) + 0x0D, 0x00, 0x00, 0x00, + + // first offset (ignored for CIE) + 0x00, 0x00, 0x00, 0x00, + + // second offset = -1 (0xFFFFFFFF) marks this as CIE + 0xFF, 0xFF, 0xFF, 0xFF, + + // version + 0x03, + + // augmentation string "x" (ASCII 0x78) + null terminator + 0x78, 0x00, + + // initial instructions + 0xAA, 0xBB, + }; + + using var reader = new DwarfMemoryReader(data); + + DwarfCommonInformationEntry[] entries = DwarfCommonInformationEntry.ParseAll(reader, defaultAddressSize: 8); + + entries.Should().HaveCount(1); + var entry = entries[0]; + + entry.Version.Should().Be(3); + entry.Augmentation.Should().Be("x"); + + // Non-empty augmentation path uses fixed defaults. + entry.AddressSize.Should().Be(4); + entry.SegmentSelectorSize.Should().Be(0); + entry.CodeAlignmentFactor.Should().Be(0); + entry.DataAlignmentFactor.Should().Be(0); + entry.ReturnAddressRegister.Should().Be(0); + + entry.InitialInstructions.Should().Equal(0xAA, 0xBB); + entry.FrameDescriptionEntries.Should().BeEmpty(); + } + + [Fact] + public void ParseAll_WithEmptyAugmentationAndVersionLessThanFour_UsesDefaultAddressSize() + { + // Version < 4 and empty augmentation string should use the provided + // defaultAddressSize and read alignment factors and return address + // via ULEB128. + + // Body layout after the two offsets: + // version (1 byte) + // augmentation string "" (null terminator only, 1 byte) + // code alignment factor (ULEB128, 1 byte) + // data alignment factor (ULEB128, 1 byte) + // return address register (ULEB128, 1 byte) + // initial instructions (1 byte) + // Total body = 6 bytes, plus two 4-byte offsets = 14. + + byte[] data = new byte[] + { + // length = 14 bytes following this field + 0x0E, 0x00, 0x00, 0x00, + + // first offset (ignored for CIE) + 0x00, 0x00, 0x00, 0x00, + + // second offset = -1 (CIE sentinel) + 0xFF, 0xFF, 0xFF, 0xFF, + + // version = 3 (< 4) + 0x03, + + // empty augmentation string (just null terminator) + 0x00, + + // CodeAlignmentFactor = 1, DataAlignmentFactor = 2, ReturnAddressRegister = 3 + 0x01, 0x02, 0x03, + + // initial instructions + 0xCC, + }; + + using var reader = new DwarfMemoryReader(data); + + DwarfCommonInformationEntry[] entries = DwarfCommonInformationEntry.ParseAll(reader, defaultAddressSize: 8); + + entries.Should().HaveCount(1); + var entry = entries[0]; + + entry.Version.Should().Be(3); + entry.Augmentation.Should().BeEmpty(); + + entry.AddressSize.Should().Be(8); // defaultAddressSize + entry.SegmentSelectorSize.Should().Be(0); + entry.CodeAlignmentFactor.Should().Be(1); + entry.DataAlignmentFactor.Should().Be(2); + entry.ReturnAddressRegister.Should().Be(3); + + entry.InitialInstructions.Should().Equal(0xCC); + } + + [Fact] + public void ParseAll_WithEmptyAugmentationAndVersionFour_ReadsExplicitAddressAndSegmentSizes() + { + // Version >= 4 with empty augmentation string should read AddressSize + // and SegmentSelectorSize explicitly from the stream before the + // alignment factors and return address register. + + // Body layout after the two offsets: + // version (1 byte) + // augmentation string "" (1 byte) + // address size (1 byte) + // segment selector size (1 byte) + // code alignment factor (ULEB128, 1 byte) + // data alignment factor (ULEB128, 1 byte) + // return address register (ULEB128, 1 byte) + // initial instructions (1 byte) + // Total body = 8 bytes, plus two 4-byte offsets = 16. + + byte[] data = new byte[] + { + // length = 16 bytes following this field + 0x10, 0x00, 0x00, 0x00, + + // first offset (ignored for CIE) + 0x00, 0x00, 0x00, 0x00, + + // second offset = -1 (CIE sentinel) + 0xFF, 0xFF, 0xFF, 0xFF, + + // version = 4 + 0x04, + + // empty augmentation string + 0x00, + + // explicit address and segment selector sizes + 0x08, // AddressSize + 0x01, // SegmentSelectorSize + + // CodeAlignmentFactor = 2, DataAlignmentFactor = 3, ReturnAddressRegister = 4 + 0x02, 0x03, 0x04, + + // initial instructions + 0xDD, + }; + + using var reader = new DwarfMemoryReader(data); + + DwarfCommonInformationEntry[] entries = DwarfCommonInformationEntry.ParseAll(reader, defaultAddressSize: 4); + + entries.Should().HaveCount(1); + var entry = entries[0]; + + entry.Version.Should().Be(4); + entry.Augmentation.Should().BeEmpty(); + + entry.AddressSize.Should().Be(8); + entry.SegmentSelectorSize.Should().Be(1); + entry.CodeAlignmentFactor.Should().Be(2); + entry.DataAlignmentFactor.Should().Be(3); + entry.ReturnAddressRegister.Should().Be(4); + + entry.InitialInstructions.Should().Equal(0xDD); + } + + [Fact] + public void ParseAll_WithFdeFollowingCie_AttachesFrameDescriptionEntryAndParsesAddresses() + { + // This test crafts a stream with one Common Information Entry (CIE) + // followed by a Frame Description Entry (FDE) that references it. + // It verifies that: + // - the FDE is associated with the CIE via FrameDescriptionEntries + // - InitialLocation, AddressRange, and Instructions are parsed + // according to the CIE's AddressSize and the record length. + + // First record: CIE with version 3, empty augmentation, defaultAddressSize = 8. + // Body after two offsets: + // version (1) + // augmentation "" (1) + // CodeAlignmentFactor (ULEB128 0) + // DataAlignmentFactor (ULEB128 0) + // ReturnAddressRegister (ULEB128 0) + // InitialInstructions (0 bytes) + // Body length = 5, plus 8 bytes of offsets = 13. + + byte[] cie = new byte[] + { + // length = 13 bytes following this field + 0x0D, 0x00, 0x00, 0x00, + + // first offset (ignored for CIE) + 0x00, 0x00, 0x00, 0x00, + + // second offset = -1 (CIE sentinel) + 0xFF, 0xFF, 0xFF, 0xFF, + + // version = 3 (< 4) + 0x03, + + // empty augmentation + 0x00, + + // CodeAlignmentFactor = 0, DataAlignmentFactor = 0, ReturnAddressRegister = 0 + 0x00, 0x00, 0x00, + }; + + // Second record: FDE that references the CIE at offset 0. + // Body after two offsets: + // InitialLocation (8 bytes, LE) + // AddressRange (8 bytes, LE) + // Instructions (2 bytes) + // Body length = 18, plus 8 bytes of offsets = 26. + + ulong initialLocation = 0x1122334455667788UL; + ulong addressRange = 0x8877665544332211UL; + + byte[] fde = new byte[4 + 4 + 4 + 8 + 8 + 2]; + + int index = 0; + + // length = 26 + fde[index++] = 0x1A; fde[index++] = 0x00; fde[index++] = 0x00; fde[index++] = 0x00; + + // first offset = 0 (reference to CIE start position) + fde[index++] = 0x00; fde[index++] = 0x00; fde[index++] = 0x00; fde[index++] = 0x00; + + // second offset != -1 so this is treated as FDE + fde[index++] = 0x00; fde[index++] = 0x00; fde[index++] = 0x00; fde[index++] = 0x00; + + // InitialLocation (8 bytes LE) + Array.Copy(BitConverter.GetBytes(initialLocation), 0, fde, index, 8); + index += 8; + + // AddressRange (8 bytes LE) + Array.Copy(BitConverter.GetBytes(addressRange), 0, fde, index, 8); + index += 8; + + // Instructions (2 bytes) + fde[index++] = 0xAA; + fde[index++] = 0xBB; + + // Combined stream: CIE followed by FDE. + byte[] data = new byte[cie.Length + fde.Length]; + Array.Copy(cie, 0, data, 0, cie.Length); + Array.Copy(fde, 0, data, cie.Length, fde.Length); + + using var reader = new DwarfMemoryReader(data); + + DwarfCommonInformationEntry[] entries = DwarfCommonInformationEntry.ParseAll(reader, defaultAddressSize: 8); + + entries.Should().HaveCount(1); + var entry = entries[0]; + + entry.AddressSize.Should().Be(8); + entry.FrameDescriptionEntries.Should().HaveCount(1); + + DwarfFrameDescriptionEntry description = entry.FrameDescriptionEntries[0]; + + description.CommonInformationEntry.Should().BeSameAs(entry); + description.InitialLocation.Should().Be(initialLocation); + description.AddressRange.Should().Be(addressRange); + description.Instructions.Should().Equal(0xAA, 0xBB); + } + } +} From 237fd1e93bb2c16d4b45b1e32665b58b53a450fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20=C5=A0vendov=C3=A1?= Date: Mon, 13 Apr 2026 15:27:04 +0200 Subject: [PATCH 03/36] Add unit tests for DwarfFrameDescriptionEntry to validate constructor behavior --- .../Dwarf/DwarfCompilationUnitFormTests.cs | 69 +++++++++++ .../Dwarf/DwarfFrameDescriptionEntryTests.cs | 109 ++++++++++++++++++ 2 files changed, 178 insertions(+) create mode 100644 src/Test.UnitTests.BinaryParsers/Dwarf/DwarfFrameDescriptionEntryTests.cs diff --git a/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfCompilationUnitFormTests.cs b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfCompilationUnitFormTests.cs index b3a929b7..210f1314 100644 --- a/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfCompilationUnitFormTests.cs +++ b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfCompilationUnitFormTests.cs @@ -1123,5 +1123,74 @@ public void ReadData_BailsOutOnUnknownAbbrevCode_PreservingValidEntries() cu.SymbolsTree[0].Tag.Should().Be(DwarfTag.CompileUnit); ((ulong)cu.SymbolsTree[0].Attributes[DwarfAttribute.Name].Value).Should().Be(0xAA); } + + [Fact] + public void ReadData_InsertsVoidBaseTypeAndDefaultsPointerTypeToVoid() + { + // Single DIE using a PointerType abbreviation with only a ByteSize attribute. + // DwarfCompilationUnit should: + // - Insert a synthetic "void" base type as a child of the first root symbol. + // - Add a Type attribute to the PointerType symbol pointing to that void type. + + byte[] dieData = new byte[] + { + 0x01, // abbreviation code = 1 + 0x08, // ByteSize = DW_FORM_data1 = 8 + }; + + byte[] header = BuildDwarf4Header((uint)dieData.Length); + byte[] debugInfo = header.Concat(dieData).ToArray(); + byte[] abbrev = BuildAbbrevSingleAttribute( + DwarfTag.PointerType, + DwarfAttribute.ByteSize, + DwarfFormat.Data1); + + using var debugData = new DwarfMemoryReader(debugInfo); + using var debugAbbrev = new DwarfMemoryReader(abbrev); + using var debugStrings = new DwarfMemoryReader(new byte[] { 0x00 }); + using var debugLineStrings = new DwarfMemoryReader(new byte[] { 0x00 }); + + var stub = new StubDwarfBinary(); + var cu = new DwarfCompilationUnit( + stub, + debugData, + debugAbbrev, + debugStrings, + debugLineStrings, + new List(), + stub.NormalizeAddress); + + cu.SymbolsTree.Should().HaveCount(1); + var pointer = cu.SymbolsTree[0]; + pointer.Tag.Should().Be(DwarfTag.PointerType); + + // Synthetic void base type is inserted as first child. + pointer.Children.Should().NotBeNull(); + pointer.Children.Should().NotBeEmpty(); + var voidSymbol = pointer.Children[0]; + voidSymbol.Tag.Should().Be(DwarfTag.BaseType); + + voidSymbol.Attributes.Should().ContainKey(DwarfAttribute.Name); + voidSymbol.Attributes[DwarfAttribute.Name].Type.Should().Be(DwarfAttributeValueType.String); + voidSymbol.Attributes[DwarfAttribute.Name].String.Should().Be("void"); + + voidSymbol.Attributes.Should().ContainKey(DwarfAttribute.ByteSize); + voidSymbol.Attributes[DwarfAttribute.ByteSize].Type.Should().Be(DwarfAttributeValueType.Constant); + ((ulong)voidSymbol.Attributes[DwarfAttribute.ByteSize].Value).Should().Be(0UL); + + // Pointer keeps its explicit ByteSize attribute. + pointer.Attributes.Should().ContainKey(DwarfAttribute.ByteSize); + pointer.Attributes[DwarfAttribute.ByteSize].Type.Should().Be(DwarfAttributeValueType.Constant); + ((ulong)pointer.Attributes[DwarfAttribute.ByteSize].Value).Should().Be(8UL); + + // And receives a Type attribute that resolves to the synthetic void symbol. + pointer.Attributes.Should().ContainKey(DwarfAttribute.Type); + var typeAttr = pointer.Attributes[DwarfAttribute.Type]; + typeAttr.Type.Should().Be(DwarfAttributeValueType.ResolvedReference); + typeAttr.Reference.Should().BeSameAs(voidSymbol); + + // NextOffset should advance to the end of the single CU. + cu.NextOffset.Should().Be((uint)debugInfo.Length); + } } } diff --git a/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfFrameDescriptionEntryTests.cs b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfFrameDescriptionEntryTests.cs new file mode 100644 index 00000000..510f7adf --- /dev/null +++ b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfFrameDescriptionEntryTests.cs @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using FluentAssertions; + +using Microsoft.CodeAnalysis.BinaryParsers.Dwarf; + +using Xunit; + +namespace Microsoft.CodeAnalysis.BinaryParsers.Dwarf +{ + /// + /// Unit tests for . + /// + public class DwarfFrameDescriptionEntryTests + { + /// + /// Minimal test-only CIE that lets us control AddressSize. + /// + private sealed class TestCommonInformationEntry : DwarfCommonInformationEntry + { + public TestCommonInformationEntry(byte addressSize) + { + AddressSize = addressSize; + } + } + + [Fact] + public void Constructor_WithFourByteAddressSize_ReadsTwoUintsAndRemainingInstructions() + { + // Layout for AddressSize = 4: + // InitialLocation (4 bytes LE) + // AddressRange (4 bytes LE) + // Instructions (3 bytes) + byte[] data = new byte[] + { + 0x78, 0x56, 0x34, 0x12, // InitialLocation = 0x12345678 + 0xEF, 0xCD, 0xAB, 0x90, // AddressRange = 0x90ABCDEF + 0xAA, 0xBB, 0xCC, // Instructions + }; + + using var reader = new DwarfMemoryReader(data); + var cie = new TestCommonInformationEntry(addressSize: 4); + + var entry = new DwarfFrameDescriptionEntry(reader, cie, endPosition: (uint)data.Length); + + entry.InitialLocation.Should().Be(0x12345678UL); + entry.AddressRange.Should().Be(0x90ABCDEFUL); + entry.Instructions.Should().Equal(0xAA, 0xBB, 0xCC); + entry.CommonInformationEntry.Should().BeSameAs(cie); + } + + [Fact] + public void Constructor_WithEightByteAddressSize_ReadsTwoUlongsAndRemainingInstructions() + { + // Layout for AddressSize = 8: + // InitialLocation (8 bytes LE) + // AddressRange (8 bytes LE) + // Instructions (2 bytes) + ulong initialLocation = 0x1122334455667788UL; + ulong addressRange = 0x8877665544332211UL; + + byte[] data = new byte[8 + 8 + 2]; + int index = 0; + + // InitialLocation + System.Array.Copy(System.BitConverter.GetBytes(initialLocation), 0, data, index, 8); + index += 8; + + // AddressRange + System.Array.Copy(System.BitConverter.GetBytes(addressRange), 0, data, index, 8); + index += 8; + + // Instructions + data[index++] = 0x01; + data[index++] = 0x02; + + using var reader = new DwarfMemoryReader(data); + var cie = new TestCommonInformationEntry(addressSize: 8); + + var entry = new DwarfFrameDescriptionEntry(reader, cie, endPosition: (uint)data.Length); + + entry.InitialLocation.Should().Be(initialLocation); + entry.AddressRange.Should().Be(addressRange); + entry.Instructions.Should().Equal(0x01, 0x02); + entry.CommonInformationEntry.Should().BeSameAs(cie); + } + + [Fact] + public void Constructor_WhenEndPositionEqualsCurrentPosition_SetsEmptyInstructions() + { + // No instruction bytes after InitialLocation and AddressRange; the + // ReadBlock call should return an empty array. + byte[] data = new byte[] + { + 0x11, 0x22, 0x33, 0x44, // InitialLocation (4 bytes) + 0x55, 0x66, 0x77, 0x88, // AddressRange (4 bytes) + }; + + using var reader = new DwarfMemoryReader(data); + var cie = new TestCommonInformationEntry(addressSize: 4); + + var entry = new DwarfFrameDescriptionEntry(reader, cie, endPosition: (uint)data.Length); + + entry.Instructions.Should().NotBeNull(); + entry.Instructions.Should().BeEmpty(); + } + } +} From 208b93979330b8da5480c013183428b0034166f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20=C5=A0vendov=C3=A1?= Date: Mon, 13 Apr 2026 15:33:18 +0200 Subject: [PATCH 04/36] Add unit tests for DwarfLineNumberProgram to validate constructor and data reading behavior --- .../Dwarf/DwarfLineNumberProgramTests.cs | 271 ++++++++++++++++++ 1 file changed, 271 insertions(+) create mode 100644 src/Test.UnitTests.BinaryParsers/Dwarf/DwarfLineNumberProgramTests.cs diff --git a/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfLineNumberProgramTests.cs b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfLineNumberProgramTests.cs new file mode 100644 index 00000000..f384ff1f --- /dev/null +++ b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfLineNumberProgramTests.cs @@ -0,0 +1,271 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +using FluentAssertions; + +using Microsoft.CodeAnalysis.BinaryParsers.Dwarf; + +using Xunit; + +namespace Microsoft.CodeAnalysis.BinaryParsers.Dwarf +{ + /// + /// Unit tests for . + /// + public class DwarfLineNumberProgramTests + { + /// + /// Helper to encode an unsigned LEB128 value into bytes. + /// Copied from DwarfCompilationUnitFormTests to keep tests self-contained. + /// + private static byte[] EncodeULEB128(ulong value) + { + var bytes = new List(); + do + { + byte b = (byte)(value & 0x7F); + value >>= 7; + if (value != 0) + { + b |= 0x80; + } + + bytes.Add(b); + } while (value != 0); + return bytes.ToArray(); + } + + [Fact] + public void Constructor_WithUnitLengthLessOrEqualToOne_ReturnsNullFiles() + { + // unit_length = 1 triggers the early-null path in ReadData. + byte[] debugLineData = new byte[] { 0x01, 0x00, 0x00, 0x00 }; + + using var debugLine = new DwarfMemoryReader(debugLineData); + using var debugStrings = new DwarfMemoryReader(new byte[] { 0x00 }); + + var program = new DwarfLineNumberProgram( + dwarfVersion: 4, + debugLine: debugLine, + debugStrings: debugStrings, + addressNormalizer: addr => addr); + + program.Files.Should().BeNull(); + } + + [Fact] + public void ReadData_Dwarf4_SingleFileSingleCopy_ProducesOneNormalizedLine() + { + // Build a minimal DWARF4 .debug_line section with: + // - one file entry + // - a single COPY opcode to emit one line record + + var body = new MemoryStream(); + using (var bw = new BinaryWriter(body, System.Text.Encoding.UTF8, leaveOpen: true)) + { + // version = 4 + bw.Write((ushort)4); + + // header_length (ignored by implementation, but required field) + bw.Write(0); // 32-bit offset + + // minimum_instruction_length + bw.Write((byte)1); + + // maximum_operations_per_instruction (version > 3) + bw.Write((byte)1); + + // default_is_stmt = 1 (true) + bw.Write((byte)1); + + // line_base (sbyte) and line_range + bw.Write(unchecked((byte)0)); // line_base = 0 + bw.Write((byte)1); // line_range = 1 + + // opcode_base and standard_opcode_lengths (1..12) + byte opcodeBase = 13; + bw.Write(opcodeBase); + for (int i = 1; i < opcodeBase; i++) + { + // All standard opcodes take 0 extra operands for this test + bw.Write((byte)0); + } + + // Directories: none — write terminator 0 + bw.Write((byte)0x00); + + // Files: one entry "file.c" with directory index 0, timestamp 0, length 0 + bw.Write(System.Text.Encoding.UTF8.GetBytes("file.c")); + bw.Write((byte)0x00); // null terminator for name + bw.Write((byte)0x00); // directory index (ULEB128 0) + bw.Write((byte)0x00); // lastModified (ULEB128 0) + bw.Write((byte)0x00); // length (ULEB128 0) + bw.Write((byte)0x00); // files terminator + + // Instructions: a single COPY opcode (standard opcode 1) + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); + } + + byte[] bodyBytes = body.ToArray(); + uint unitLength = (uint)bodyBytes.Length; + + byte[] debugLineData = BitConverter.GetBytes(unitLength) + .Concat(bodyBytes) + .Concat(new byte[] { 0x00 }) // extra padding byte so endPosition stays within bounds + .ToArray(); + + using var debugLine = new DwarfMemoryReader(debugLineData); + using var debugStrings = new DwarfMemoryReader(new byte[] { 0x00 }); + + // Normalize addresses by adding a fixed offset to prove post-processing ran. + uint normalizeDelta = 0x10; + var program = new DwarfLineNumberProgram( + dwarfVersion: 4, + debugLine: debugLine, + debugStrings: debugStrings, + addressNormalizer: addr => addr + normalizeDelta); + + program.Files.Should().NotBeNull(); + program.Files.Should().HaveCount(1); + + var file = program.Files[0]; + file.Name.Should().Be("file.c"); + file.Lines.Should().HaveCount(1); + + var line = file.Lines[0]; + line.File.Should().BeSameAs(file); + line.Line.Should().Be(1u); // initial line + line.Column.Should().Be(0ul); // default column + line.Address.Should().Be(normalizeDelta); // 0 + normalizeDelta + } + + [Fact] + public void ReadData_Dwarf5_DirectoriesAndFiles_UseStrpAndDirectoryIndex() + { + // Build DWARF5 .debug_line section exercising the DWARF5-specific + // directory and file tables using Strp and DirectoryIndex entries. + + // .debug_line/.debug_line_str (or shared debugStrings) data: + // offset 0: "dir\0" + // offset 4: "foo.c\0" + byte[] debugStringsData = + { + (byte)'d', (byte)'i', (byte)'r', 0x00, + (byte)'f', (byte)'o', (byte)'o', (byte)'.', (byte)'c', 0x00, + }; + + var body = new MemoryStream(); + using (var bw = new BinaryWriter(body, System.Text.Encoding.UTF8, leaveOpen: true)) + { + // version = 5 + bw.Write((ushort)5); + + // address_size and segment_selector_size (ignored by tests) + bw.Write((byte)8); // address_size + bw.Write((byte)0); // segmentSelectorSize + + // header_length (offset; implementation does not use it for skipping) + bw.Write(0); // 32-bit + + // minimum_instruction_length + bw.Write((byte)1); + + // maximum_operations_per_instruction (version > 3) + bw.Write((byte)1); + + // default_is_stmt = 1 + bw.Write((byte)1); + + // line_base, line_range + bw.Write(unchecked((byte)0)); // line_base = 0 + bw.Write((byte)1); // line_range = 1 + + // opcode_base and standard_opcode_lengths + byte opcodeBase = 13; + bw.Write(opcodeBase); + for (int i = 1; i < opcodeBase; i++) + { + bw.Write((byte)0); + } + + // ---- DWARF5 directory table ---- + + // directoryEntryFormatCount = 1 + bw.Write((byte)1); + + // Entry 0: Path (Strp) + bw.Write(EncodeULEB128((ulong)DwarfLineNumberHeaderEntryFormat.Path)); + bw.Write(EncodeULEB128((ulong)DwarfFormat.Strp)); + + // directoriesCount = 1 + bw.Write(EncodeULEB128(1)); + + // Directory 0: path at offset 0 in debugStringsData + bw.Write(BitConverter.GetBytes(0)); + + // ---- DWARF5 file table ---- + + // fileEntryFormatCount = 2 + bw.Write((byte)2); + + // Entry 0: Path (Strp) + bw.Write(EncodeULEB128((ulong)DwarfLineNumberHeaderEntryFormat.Path)); + bw.Write(EncodeULEB128((ulong)DwarfFormat.Strp)); + + // Entry 1: DirectoryIndex (Data1) + bw.Write(EncodeULEB128((ulong)DwarfLineNumberHeaderEntryFormat.DirectoryIndex)); + bw.Write(EncodeULEB128((ulong)DwarfFormat.Data1)); + + // filesCount = 1 + bw.Write(EncodeULEB128(1)); + + // File 0: name at offset 4, directory index = 0 (implementation treats this as first directory) + bw.Write(BitConverter.GetBytes(4)); // Strp offset to "foo.c" + bw.Write((byte)0); // directory index (Data1) + + // ---- Opcodes ---- + + // Single COPY to emit one line record. + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); + } + + byte[] bodyBytes = body.ToArray(); + uint unitLength = (uint)bodyBytes.Length; + + byte[] debugLineData = BitConverter.GetBytes(unitLength) + .Concat(bodyBytes) + .Concat(new byte[] { 0x00 }) // extra padding byte so endPosition stays within bounds + .ToArray(); + + using var debugLine = new DwarfMemoryReader(debugLineData); + using var debugStrings = new DwarfMemoryReader(debugStringsData); + + var program = new DwarfLineNumberProgram( + dwarfVersion: 5, + debugLine: debugLine, + debugStrings: debugStrings, + addressNormalizer: addr => addr); + + program.Files.Should().NotBeNull(); + program.Files.Should().HaveCount(1); + + var file = program.Files[0]; + file.Name.Should().Be("foo.c"); + file.Directory.Should().Be("dir"); + // Path combines directory and name; we only check that both segments appear. + file.Path.Should().Contain("foo.c"); + file.Path.Should().Contain("dir"); + + file.Lines.Should().HaveCount(1); + var line = file.Lines[0]; + line.File.Should().BeSameAs(file); + line.Line.Should().Be(1u); + line.Address.Should().Be(0u); + } + } +} From db52ee50bf5b3fe1f4db686865f3f3ba9d5bbd07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20=C5=A0vendov=C3=A1?= Date: Mon, 13 Apr 2026 15:39:52 +0200 Subject: [PATCH 05/36] Add unit tests for DwarfSymbol to validate name and full name behavior --- .../Dwarf/DwarfSymbolTests.cs | 198 ++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 src/Test.UnitTests.BinaryParsers/Dwarf/DwarfSymbolTests.cs diff --git a/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfSymbolTests.cs b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfSymbolTests.cs new file mode 100644 index 00000000..24d6d490 --- /dev/null +++ b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfSymbolTests.cs @@ -0,0 +1,198 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; + +using FluentAssertions; + +using Microsoft.CodeAnalysis.BinaryParsers.Dwarf; + +using Xunit; + +namespace Microsoft.CodeAnalysis.BinaryParsers.Dwarf +{ + /// + /// Unit tests for . + /// + public class DwarfSymbolTests + { + [Fact] + public void Name_ReturnsNull_WhenNameAttributeMissing() + { + var symbol = new DwarfSymbol + { + Tag = DwarfTag.Variable, + Attributes = new Dictionary() + }; + + symbol.Name.Should().BeNull(); + } + + [Fact] + public void Name_ReturnsStringAttribute_WhenPresent() + { + var attributes = new Dictionary + { + { + DwarfAttribute.Name, + new DwarfAttributeValue + { + Type = DwarfAttributeValueType.String, + Value = "foo" + } + } + }; + + var symbol = new DwarfSymbol + { + Tag = DwarfTag.Variable, + Attributes = attributes + }; + + symbol.Name.Should().Be("foo"); + } + + [Fact] + public void FullName_ReturnsSimpleName_WhenNoParentOrCompileUnitParent() + { + var attributes = new Dictionary + { + { + DwarfAttribute.Name, + new DwarfAttributeValue + { + Type = DwarfAttributeValueType.String, + Value = "Top" + } + } + }; + + var compileUnit = new DwarfSymbol + { + Tag = DwarfTag.CompileUnit, + Attributes = new Dictionary() + }; + + var child = new DwarfSymbol + { + Tag = DwarfTag.Subprogram, + Attributes = attributes, + Parent = compileUnit + }; + + child.FullName.Should().Be("Top"); + } + + [Fact] + public void FullName_BuildsQualifiedName_ThroughNonCompileUnitParents() + { + // Build hierarchy: ns::Class::Method + var ns = new DwarfSymbol + { + Tag = DwarfTag.Namespace, + Attributes = new Dictionary + { + { + DwarfAttribute.Name, + new DwarfAttributeValue + { + Type = DwarfAttributeValueType.String, + Value = "ns" + } + } + } + }; + + var cls = new DwarfSymbol + { + Tag = DwarfTag.ClassType, + Attributes = new Dictionary + { + { + DwarfAttribute.Name, + new DwarfAttributeValue + { + Type = DwarfAttributeValueType.String, + Value = "Class" + } + } + }, + Parent = ns + }; + + var method = new DwarfSymbol + { + Tag = DwarfTag.Subprogram, + Attributes = new Dictionary + { + { + DwarfAttribute.Name, + new DwarfAttributeValue + { + Type = DwarfAttributeValueType.String, + Value = "Method" + } + } + }, + Parent = cls + }; + + method.FullName.Should().Be("ns::Class::Method"); + } + + [Fact] + public void GetConstantAttribute_ReturnsDefault_WhenAttributeMissing() + { + var symbol = new DwarfSymbol + { + Tag = DwarfTag.Variable, + Attributes = new Dictionary() + }; + + symbol.GetConstantAttribute(DwarfAttribute.ByteSize, defaultValue: 42).Should().Be(42UL); + } + + [Fact] + public void GetConstantAttribute_ReturnsUnderlyingConstant_WhenPresent() + { + var attributes = new Dictionary + { + { + DwarfAttribute.ByteSize, + new DwarfAttributeValue + { + Type = DwarfAttributeValueType.Constant, + Value = 8UL + } + } + }; + + var symbol = new DwarfSymbol + { + Tag = DwarfTag.Variable, + Attributes = attributes + }; + + symbol.GetConstantAttribute(DwarfAttribute.ByteSize, defaultValue: 0).Should().Be(8UL); + } + + [Fact] + public void ToString_IncludesTagOffsetAttributeAndChildCounts() + { + var symbol = new DwarfSymbol + { + Tag = DwarfTag.Variable, + Attributes = new Dictionary(), + Children = new List(), + }; + + // Offset is internal but defaults to 0. + string text = symbol.ToString(); + + text.Should().Contain("Variable"); + text.Should().Contain("Offset = 0"); + text.Should().Contain("Attributes = 0"); + text.Should().Contain("Children = 0"); + } + } +} From d57aba569850b9a0de4c0c416fe54cb5df2e4af1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20=C5=A0vendov=C3=A1?= Date: Mon, 13 Apr 2026 16:32:09 +0200 Subject: [PATCH 06/36] Add unit tests for DwarfSymbolProvider to validate parsing and command line info extraction --- .../Dwarf/DwarfSymbolProviderTests.cs | 380 ++++++++++++++++++ 1 file changed, 380 insertions(+) create mode 100644 src/Test.UnitTests.BinaryParsers/Dwarf/DwarfSymbolProviderTests.cs diff --git a/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfSymbolProviderTests.cs b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfSymbolProviderTests.cs new file mode 100644 index 00000000..2a9c04d6 --- /dev/null +++ b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfSymbolProviderTests.cs @@ -0,0 +1,380 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; + +using FluentAssertions; + +using Microsoft.CodeAnalysis.BinaryParsers; +using Microsoft.CodeAnalysis.BinaryParsers.Dwarf; + +using Xunit; + +namespace Microsoft.CodeAnalysis.BinaryParsers.Dwarf +{ + /// + /// Unit tests for . + /// + public class DwarfSymbolProviderTests + { + /// + /// Minimal IDwarfBinary stub needed by ParseLineNumberPrograms. + /// + private class StubDwarfBinary : IDwarfBinary + { + public int DwarfVersion { get; set; } + public DwarfUnitType DwarfUnitType { get; set; } + public byte[] DebugData { get; } = Array.Empty(); + public byte[] DebugDataDescription { get; } = Array.Empty(); + public byte[] DebugDataStrings { get; } = Array.Empty(); + public byte[] DebugLine { get; } = Array.Empty(); + public byte[] DebugFrame { get; } = Array.Empty(); + public byte[] EhFrame { get; } = Array.Empty(); + public ulong CodeSegmentOffset { get; } = 0; + public ulong EhFrameAddress { get; } = 0; + public ulong TextSectionAddress { get; } = 0; + public ulong DataSectionAddress { get; } = 0; + public IReadOnlyList PublicSymbols { get; } = Array.Empty(); + public bool Is64bit { get; } = false; + public ICompiler[] Compilers { get; } = Array.Empty(); + public List CommandLineInfos { get; } = new List(); + public ulong NormalizeAddress(ulong address) => address; + public DwarfLanguage GetLanguage() => DwarfLanguage.C; + public void Dispose() { } + } + + /// + /// Helper to encode an unsigned LEB128 value into bytes. + /// Copied from DwarfCompilationUnitFormTests / DwarfLineNumberProgramTests to keep tests self-contained. + /// + private static byte[] EncodeULEB128(ulong value) + { + var bytes = new List(); + do + { + byte b = (byte)(value & 0x7F); + value >>= 7; + if (value != 0) + { + b |= 0x80; + } + + bytes.Add(b); + } while (value != 0); + return bytes.ToArray(); + } + + // ---- ParseDebugStringOffsets ---- + + [Fact] + public void ParseDebugStringOffsets_Reads32BitOffsets() + { + byte[] data = BitConverter.GetBytes(1u) + .Concat(BitConverter.GetBytes(2u)) + .ToArray(); + + List offsets = DwarfSymbolProvider.ParseDebugStringOffsets(data, is64bit: false); + + offsets.Should().Equal(1, 2); + } + + [Fact] + public void ParseDebugStringOffsets_Reads64BitOffsets() + { + byte[] data = BitConverter.GetBytes(1ul) + .Concat(BitConverter.GetBytes(2ul)) + .ToArray(); + + List offsets = DwarfSymbolProvider.ParseDebugStringOffsets(data, is64bit: true); + + offsets.Should().Equal(1, 2); + } + + // ---- ParseLineNumberPrograms ---- + + [Fact] + public void ParseLineNumberPrograms_WithEmptyDebugLine_ReturnsEmptyList() + { + var dwarfBinary = new StubDwarfBinary { DwarfVersion = 4 }; + + List programs = DwarfSymbolProvider.ParseLineNumberPrograms( + dwarfBinary, + debugLine: Array.Empty(), + debugStrings: new byte[] { 0x00 }, + debugLineStrings: new byte[] { 0x00 }, + addressNormalizer: addr => addr); + + programs.Should().NotBeNull(); + programs.Should().BeEmpty(); + } + + [Fact] + public void ParseLineNumberPrograms_StopsWhenProgramFilesIsNull() + { + var dwarfBinary = new StubDwarfBinary { DwarfVersion = 4 }; + + // unit_length = 1 triggers the early-null path in DwarfLineNumberProgram.ReadData. + byte[] debugLineData = new byte[] { 0x01, 0x00, 0x00, 0x00 }; + + List programs = DwarfSymbolProvider.ParseLineNumberPrograms( + dwarfBinary, + debugLine: debugLineData, + debugStrings: new byte[] { 0x00 }, + debugLineStrings: new byte[] { 0x00 }, + addressNormalizer: addr => addr); + + programs.Should().NotBeNull(); + programs.Should().BeEmpty(); + } + + [Fact] + public void ParseLineNumberPrograms_Dwarf4SingleProgram_ProducesOneNormalizedLine() + { + // Build a minimal DWARF4 .debug_line section with one file and a single COPY opcode, + // mirroring DwarfLineNumberProgramTests but exercising the aggregator in DwarfSymbolProvider. + + var body = new MemoryStream(); + using (var bw = new BinaryWriter(body, System.Text.Encoding.UTF8, leaveOpen: true)) + { + // version = 4 + bw.Write((ushort)4); + + // header_length (ignored by implementation, but required field) + bw.Write(0); // 32-bit offset + + // minimum_instruction_length + bw.Write((byte)1); + + // maximum_operations_per_instruction (version > 3) + bw.Write((byte)1); + + // default_is_stmt = 1 (true) + bw.Write((byte)1); + + // line_base (sbyte) and line_range + bw.Write(unchecked((byte)0)); // line_base = 0 + bw.Write((byte)1); // line_range = 1 + + // opcode_base and standard_opcode_lengths (1..12) + byte opcodeBase = 13; + bw.Write(opcodeBase); + for (int i = 1; i < opcodeBase; i++) + { + // All standard opcodes take 0 extra operands for this test + bw.Write((byte)0); + } + + // Directories: none — write terminator 0 + bw.Write((byte)0x00); + + // Files: one entry "file.c" with directory index 0, timestamp 0, length 0 + bw.Write(System.Text.Encoding.UTF8.GetBytes("file.c")); + bw.Write((byte)0x00); // null terminator for name + bw.Write((byte)0x00); // directory index (ULEB128 0) + bw.Write((byte)0x00); // lastModified (ULEB128 0) + bw.Write((byte)0x00); // length (ULEB128 0) + bw.Write((byte)0x00); // files terminator + + // Instructions: a single COPY opcode (standard opcode 1) + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); + } + + byte[] bodyBytes = body.ToArray(); + uint unitLength = (uint)bodyBytes.Length; + + // Append 4 padding bytes so that the aggregator's loop terminates cleanly + // after the valid program. + byte[] debugLineData = BitConverter.GetBytes(unitLength) + .Concat(bodyBytes) + .Concat(new byte[] { 0x00, 0x00, 0x00, 0x00 }) + .ToArray(); + + var dwarfBinary = new StubDwarfBinary { DwarfVersion = 4 }; + + uint normalizeDelta = 0x10; + List programs = DwarfSymbolProvider.ParseLineNumberPrograms( + dwarfBinary, + debugLine: debugLineData, + debugStrings: new byte[] { 0x00 }, + debugLineStrings: new byte[] { 0x00 }, + addressNormalizer: addr => addr + normalizeDelta); + + programs.Should().HaveCount(1); + + DwarfLineNumberProgram program = programs[0]; + program.Files.Should().NotBeNull(); + program.Files.Should().HaveCount(1); + + DwarfFileInformation file = program.Files[0]; + file.Name.Should().Be("file.c"); + file.Lines.Should().HaveCount(1); + + DwarfLineInformation line = file.Lines[0]; + line.File.Should().BeSameAs(file); + line.Line.Should().Be(1u); + line.Column.Should().Be(0ul); + line.Address.Should().Be(normalizeDelta); + } + + // ---- ParseAllCommandLineInfos ---- + + [Fact] + public void ParseAllCommandLineInfos_CompileUnitWithSupportedLanguage_ProducesInfo() + { + var attributes = new Dictionary + { + { DwarfAttribute.Name, new DwarfAttributeValue { Type = DwarfAttributeValueType.String, Value = "/src/main.c" } }, + { DwarfAttribute.CompDir, new DwarfAttributeValue { Type = DwarfAttributeValueType.String, Value = "/src" } }, + { DwarfAttribute.Producer, new DwarfAttributeValue { Type = DwarfAttributeValueType.String, Value = "gcc -o main main.c" } }, + { DwarfAttribute.Language, new DwarfAttributeValue { Type = DwarfAttributeValueType.Constant, Value = (ulong)DwarfLanguage.CPlusPlus11 } }, + }; + + var symbol = new DwarfSymbol + { + Tag = DwarfTag.CompileUnit, + Attributes = attributes, + }; + + DwarfCompilationUnit cu = CreateCompilationUnitWithSymbols(symbol); + + List infos = DwarfSymbolProvider.ParseAllCommandLineInfos( + new List { cu }); + + infos.Should().HaveCount(1); + DwarfCompileCommandLineInfo info = infos[0]; + + info.Type.Should().Be(DwarfTag.CompileUnit); + info.FullName.Should().Be("/src/main.c"); + info.CompileDirectory.Should().Be("/src"); + info.FileName.Should().Be("main.c"); + info.CommandLine.Should().Be("gcc -o main main.c"); + info.Language.Should().Be(DwarfLanguage.CPlusPlus11); + } + + [Fact] + public void ParseAllCommandLineInfos_CompileUnitWithUnsupportedLanguage_IsSkipped() + { + var attributes = new Dictionary + { + { DwarfAttribute.Name, new DwarfAttributeValue { Type = DwarfAttributeValueType.String, Value = "mod.f" } }, + { DwarfAttribute.Producer, new DwarfAttributeValue { Type = DwarfAttributeValueType.String, Value = "gfortran mod.f" } }, + { DwarfAttribute.Language, new DwarfAttributeValue { Type = DwarfAttributeValueType.Constant, Value = (ulong)DwarfLanguage.Fortran77 } }, + }; + + var symbol = new DwarfSymbol + { + Tag = DwarfTag.CompileUnit, + Attributes = attributes, + }; + + DwarfCompilationUnit cu = CreateCompilationUnitWithSymbols(symbol); + + List infos = DwarfSymbolProvider.ParseAllCommandLineInfos( + new List { cu }); + + infos.Should().BeEmpty(); + } + + [Fact] + public void ParseAllCommandLineInfos_SubprogramUsesLinkageName() + { + var attributes = new Dictionary + { + { DwarfAttribute.Name, new DwarfAttributeValue { Type = DwarfAttributeValueType.String, Value = "foo" } }, + { DwarfAttribute.LinkageName, new DwarfAttributeValue { Type = DwarfAttributeValueType.String, Value = "_Z3foov" } }, + }; + + var symbol = new DwarfSymbol + { + Tag = DwarfTag.Subprogram, + Attributes = attributes, + }; + + DwarfCompilationUnit cu = CreateCompilationUnitWithSymbols(symbol); + + List infos = DwarfSymbolProvider.ParseAllCommandLineInfos( + new List { cu }); + + infos.Should().HaveCount(1); + DwarfCompileCommandLineInfo info = infos[0]; + + info.Type.Should().Be(DwarfTag.Subprogram); + info.FullName.Should().Be("foo"); + info.FileName.Should().Be("foo"); + info.CommandLine.Should().Be("_Z3foov"); + info.Language.Should().Be(DwarfLanguage.Unknown); + } + + [Fact] + public void ParseAllCommandLineInfos_StripsNumericAndLongUnsignedIntNames() + { + var attributes = new Dictionary + { + { DwarfAttribute.Name, new DwarfAttributeValue { Type = DwarfAttributeValueType.String, Value = "1234" } }, + { DwarfAttribute.CompDir, new DwarfAttributeValue { Type = DwarfAttributeValueType.String, Value = ElfUtility.LongUnsignedInt } }, + { DwarfAttribute.Producer, new DwarfAttributeValue { Type = DwarfAttributeValueType.String, Value = "gcc 1234" } }, + { DwarfAttribute.Language, new DwarfAttributeValue { Type = DwarfAttributeValueType.Constant, Value = (ulong)DwarfLanguage.C } }, + }; + + var symbol = new DwarfSymbol + { + Tag = DwarfTag.CompileUnit, + Attributes = attributes, + }; + + DwarfCompilationUnit cu = CreateCompilationUnitWithSymbols(symbol); + + List infos = DwarfSymbolProvider.ParseAllCommandLineInfos( + new List { cu }); + + infos.Should().HaveCount(1); + DwarfCompileCommandLineInfo info = infos[0]; + + info.FullName.Should().BeEmpty(); + info.CompileDirectory.Should().BeEmpty(); + info.CommandLine.Should().Be("gcc 1234"); + } + + private static DwarfCompilationUnit CreateCompilationUnitWithSymbols(params DwarfSymbol[] symbols) + { + // Create a real DwarfCompilationUnit instance with a minimal, version-0 header + // so that ReadData bails out early, then inject our own symbol table via reflection. + var dwarfBinary = new StubDwarfBinary(); + + // 4-byte unit_length (0) + 2-byte version (0) is enough for ReadData + // to hit the "version == 0" early-return path without reading further. + using var debugData = new DwarfMemoryReader(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }); + using var debugDataDescription = new DwarfMemoryReader(Array.Empty()); + using var debugStrings = new DwarfMemoryReader(Array.Empty()); + using var debugLineStrings = new DwarfMemoryReader(Array.Empty()); + IList debugStringOffsets = Array.Empty(); + + var cu = new DwarfCompilationUnit( + dwarfBinary, + debugData, + debugDataDescription, + debugStrings, + debugLineStrings, + debugStringOffsets, + address => address); + + FieldInfo field = typeof(DwarfCompilationUnit).GetField("symbolsByOffset", BindingFlags.Instance | BindingFlags.NonPublic); + field.Should().NotBeNull("DwarfCompilationUnit must have a symbolsByOffset field for tests to seed symbols"); + + var map = new Dictionary(); + uint offset = 0; + foreach (DwarfSymbol symbol in symbols) + { + map[offset++] = symbol; + } + + field.SetValue(cu, map); + + return cu; + } + } +} From 36ec1d4a3a7762ea9228bc87408260de7672101b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20=C5=A0vendov=C3=A1?= Date: Wed, 22 Apr 2026 18:01:39 +0200 Subject: [PATCH 07/36] Update active context and learnings with guardian integration test details; add helper methods for DWARF line number program tests --- .memory-bank/activeContext.md | 67 +- .memory-bank/learnings.md | 14 + .../Dwarf/DwarfLineNumberProgramTests.cs | 574 ++++++++++++++++++ 3 files changed, 642 insertions(+), 13 deletions(-) diff --git a/.memory-bank/activeContext.md b/.memory-bank/activeContext.md index 2cfa8188..4216bdcd 100644 --- a/.memory-bank/activeContext.md +++ b/.memory-bank/activeContext.md @@ -1,24 +1,65 @@ # Active Context ## Current Work Focus -Design and plan unit tests for DWARF-related types, starting with DwarfAttributeValue and including DWARF4/DWARF5-specific behaviors. +Planning guardian integration tests for BinSkim. ## Recent Changes -- ✅ Defined detailed unit test coverage goals for DwarfAttributeValue (all enum values, accessors, equality, hash code, and ToString). -- ✅ Identified DWARF4 and DWARF5 form mappings in DwarfCompilationUnit that feed into DwarfAttributeValue. +- (none - new work focus) ## Active Decisions -- ✅ Full enum coverage for DwarfAttributeValueType is desired, including currently less-used values like Invalid, ResolvedReference, and Loclistx. -- ✅ Tests should pin current behavior even when it may be surprising (e.g., equality/hash interactions for unhandled enum types), so future changes are explicit. +- Pending: scope clarification needed before plan creation (see Open Questions below) + +## Research Findings + +### What Guardian Means in This Repo +- Guardian = Microsoft 1ES SDL infrastructure pools (guardian-build-infra-*) +- 1ESPT = 1 Engineering System Perf and Test pool for compliance scanning +- NOT a NuGet package, NOT a separate tool - it is infrastructure +- No .gdnconfig or .gdn files exist in the repo + +### Existing Pipelines +- .pipelines/1ES.binskim.BuildAndTest.EXTERNAL.yml - build/test pipeline on guardian-build-infra pools (Linux + Windows matrix), triggers internal pipeline ID 21327 +- .pipelines/1ES.binskim.compliance.EXTERNAL.yml - compliance/SDL pipeline on pool-1espt-mseng, runs BinSkim v4.3.1, CodeQL, CredScan, SPMI on built outputs +- Build uses 1ES.Official.PipelineTemplate.yml, compliance uses 1ES.Unofficial.PipelineTemplate.yml + +### Compliance Pipeline Details (1ES.binskim.compliance.EXTERNAL.yml) +- Trigger: main branch only +- Builds with BuildAndTest.cmd, then runs BinSkim@4 task (exact version 4.3.1) +- AnalyzeTargetGlob scans net9.0 release output DLLs and EXEs +- AnalyzeSymPath uses Cache + symweb +- Also runs CredScan@2, publishes security analysis logs + +### Build Pipeline Details (1ES.binskim.BuildAndTest.EXTERNAL.yml) +- Trigger: every PR and commit +- Matrix: Linux (guardian-build-infra-linux-x64) and Windows (guardian-build-infra-windows-x64) +- Uses custom NuGet feed: BinSkim.Build on mseng/1ES +- Has ValidateReleasePRs stage (checks PRs in ReleaseHistory.md are merged) +- Has InternalValidation stage (triggers internal BinSkimInternal pipeline ID 21327, polls for completion) + +### Existing Test Patterns +- Test.FunctionalTests.BinSkim.Driver - end-to-end driver/baseline tests with expected SARIF outputs +- Test.FunctionalTests.BinSkim.Rules - rule functional tests with Pass/Fail test data folders +- Test.UnitTests.* - unit test projects +- Tech stack: xUnit, FluentAssertions, .NET 9 +- Test assets include PE, ELF, Mach-O binaries with various compiler versions +- Baseline tests compare actual SARIF output against Expected/ and NonWindowsExpected/ folders + +### Current State - No Guardian Integration Tests Exist +- No dedicated guardian/1ESPT integration test suite +- No cross-project validation tests +- BinSkim self-scans in compliance pipeline but no formal integration test suite + +## Open Questions (Asked, Awaiting Response) +Scope clarification: What does "guardian integration tests" mean? +- Option A: New ADO pipeline YAML that runs BinSkim on curated test binaries on 1ESPT infrastructure, validates SARIF/SDL reporting end-to-end +- Option B: Local xUnit integration tests that invoke BinSkim as external process with same args as compliance pipeline, validate SARIF output - runnable without 1ESPT +- Option C: Both pipeline-based + local integration tests +- Option D: Something else ## Next Steps -1. ✅ Create a new DwarfAttributeValue-focused unit test class in the BinaryParsers unit test project, following existing naming and structure conventions. -2. ✅ Add tests for each accessor (Address, Block, Constant, String, Flag, Reference, ExpressionLocation, SecOffset) covering “happy path” usage. -3. ✅ Add Constant decoding tests for byte[] lengths 1/2/4/8, unsupported lengths (throw NotImplementedException), and direct ulong Value. -4. ✅ Add equality/hash code tests covering null semantics, type mismatches, handled enum types, and unhandled enum types (Invalid, ResolvedReference, Loclistx). -5. ✅ Add DWARF4-oriented tests that verify DwarfCompilationUnit maps classic forms (Data1/2/4/8, SData, UData, Address, Block*, String/Strp, Flag*, Ref*) into the expected DwarfAttributeValue Type/Value combinations. -6. ✅ Add DWARF5-oriented tests that verify newer forms (LineStrp, StrpSup, Strx*/GNUStrIndex, Addrx*/GNUAddrIndex, Rnglistx, Loclistx, RefSup*, RefSig8) are mapped to the correct Type/Value/Offset in DwarfAttributeValue. -7. ✅ Add a couple of small end-to-end DWARF4 and DWARF5 synthetic units that exercise representative attributes and assert on the resulting DwarfAttributeValue objects. +1. Get scope clarification from user (Options A/B/C/D above) +2. Create detailed implementation plan based on answer +3. Decompose into tracks and tasks per CoDev workflow ## Current State -All planned DWARF-related unit tests, including end-to-end DWARF4 and DWARF5 synthetic units, are implemented, and the BinaryParsers unit tests are currently passing. +Research complete. Awaiting user decision on scope before creating implementation plan. diff --git a/.memory-bank/learnings.md b/.memory-bank/learnings.md index 9679edb1..6ede4968 100644 --- a/.memory-bank/learnings.md +++ b/.memory-bank/learnings.md @@ -4,3 +4,17 @@ - DwarfAttributeValue represents DWARF attribute values with a Type enum (DwarfAttributeValueType) and a boxed Value, plus an optional Offset used for deferred resolution. - Equality in DwarfAttributeValue is type-sensitive and uses special handling for some enum values (Address, Constant, Reference, SecOffset, Block, ExpressionLocation, Flag, String); other enum values currently fall through to a default “equal” path regardless of Value, which is important to keep in mind when adding tests. - DwarfCompilationUnit maps DWARF4 forms (Data*, Address, Block*, String/Strp, Flag*, Ref*, ExpressionLocation, SecOffset, etc.) and DWARF5/extended forms (LineStrp, StrpSup, Strx*/GNUStrIndex, Addrx*/GNUAddrIndex, Rnglistx, Loclistx, RefSup*, RefSig8) into DwarfAttributeValue instances by setting Type, Value, and/or Offset according to the DWARF spec and LLVM’s DWARFFormValue behavior. +## Guardian / 1ES Infrastructure +- "Guardian" in this repo refers to Microsoft 1ES SDL infrastructure agent pools (guardian-build-infra-windows-x64, guardian-build-infra-linux-x64), NOT a separate tool or NuGet package. +- 1ESPT (1 Engineering System Perf and Test) is the compliance scanning pool (pool-1espt-mseng). +- Pipeline templates come from 1ESPipelineTemplates/1ESPipelineTemplates repo (refs/tags/release): Official template for build, Unofficial for compliance. +- Compliance pipeline runs BinSkim@4 (v4.3.1), CodeQL, CredScan@2, SPMI on net9.0 release outputs. +- Build pipeline uses custom NuGet feed (BinSkim.Build on mseng/1ES), validates release PRs are merged, triggers internal BinSkimInternal pipeline (ID 21327). +- No .gdnconfig/.gdn guardian config files exist; all config is in pipeline YAML. + +## Test Infrastructure +- Functional tests use xUnit + FluentAssertions on .NET 9 (netcoreapp9.0). +- Test.FunctionalTests.BinSkim.Driver uses baseline SARIF comparison (Expected/ vs NonWindowsExpected/ folders). +- Test.FunctionalTests.BinSkim.Rules uses Pass/Fail binary folders per rule (BAXXX.RuleFriendlyName pattern). +- Test assets include PE (exe/dll), ELF, Mach-O binaries from various compilers. +- UpdateBaselines.ps1 / .sh scripts regenerate expected SARIF outputs. \ No newline at end of file diff --git a/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfLineNumberProgramTests.cs b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfLineNumberProgramTests.cs index f384ff1f..0e16a641 100644 --- a/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfLineNumberProgramTests.cs +++ b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfLineNumberProgramTests.cs @@ -267,5 +267,579 @@ public void ReadData_Dwarf5_DirectoriesAndFiles_UseStrpAndDirectoryIndex() line.Line.Should().Be(1u); line.Address.Should().Be(0u); } + + #region Helpers + + /// + /// Encode a signed LEB128 value into bytes. + /// + private static byte[] EncodeSLEB128(int value) + { + var bytes = new List(); + bool more = true; + while (more) + { + byte b = (byte)(value & 0x7F); + value >>= 7; + if ((value == 0 && (b & 0x40) == 0) || (value == -1 && (b & 0x40) != 0)) + { + more = false; + } + else + { + b |= 0x80; + } + + bytes.Add(b); + } + return bytes.ToArray(); + } + + /// + /// Builds a minimal DWARF4 .debug_line section and returns the parsed program. + /// + private static DwarfLineNumberProgram BuildDwarf4Program( + Action writeOpcodes, + byte minimumInstructionLength = 1, + sbyte lineBase = 0, + byte lineRange = 1, + byte operationCodeBase = 13, + string[] fileNames = null, + string[] directories = null, + NormalizeAddressDelegate addressNormalizer = null, + int dwarfVersion = 4) + { + fileNames ??= new[] { "file.c" }; + addressNormalizer ??= addr => addr; + + var body = new MemoryStream(); + using (var bw = new BinaryWriter(body, System.Text.Encoding.UTF8, leaveOpen: true)) + { + bw.Write((ushort)dwarfVersion); + + if (dwarfVersion > 4) + { + bw.Write((byte)8); // addressSize + bw.Write((byte)0); // segmentSelectorSize + } + + bw.Write(0); // header_length (32-bit) + bw.Write(minimumInstructionLength); + + if (dwarfVersion > 3) + { + bw.Write((byte)1); // maximumOperationsPerInstruction + } + + bw.Write((byte)1); // default_is_stmt + bw.Write(unchecked((byte)lineBase)); + bw.Write(lineRange); + bw.Write(operationCodeBase); + + for (int i = 1; i < operationCodeBase; i++) + { + bw.Write((byte)0); + } + + // Directories + if (directories != null) + { + foreach (string dir in directories) + { + bw.Write(System.Text.Encoding.UTF8.GetBytes(dir)); + bw.Write((byte)0x00); + } + } + bw.Write((byte)0x00); // directory terminator + + // Files + foreach (string fileName in fileNames) + { + bw.Write(System.Text.Encoding.UTF8.GetBytes(fileName)); + bw.Write((byte)0x00); // null terminator + bw.Write((byte)0x00); // directory index + bw.Write((byte)0x00); // timestamp + bw.Write((byte)0x00); // length + } + bw.Write((byte)0x00); // file terminator + + writeOpcodes(bw); + } + + byte[] bodyBytes = body.ToArray(); + uint unitLength = (uint)bodyBytes.Length; + + byte[] debugLineData = BitConverter.GetBytes(unitLength) + .Concat(bodyBytes) + .Concat(new byte[] { 0x00 }) + .ToArray(); + + using var debugLine = new DwarfMemoryReader(debugLineData); + using var debugStrings = new DwarfMemoryReader(new byte[] { 0x00 }); + + return new DwarfLineNumberProgram( + dwarfVersion: dwarfVersion, + debugLine: debugLine, + debugStrings: debugStrings, + addressNormalizer: addressNormalizer); + } + + /// + /// Writes a SetAddress extended opcode. + /// + private static void WriteSetAddress(BinaryWriter bw, uint address) + { + bw.Write((byte)0x00); // extended marker + bw.Write(EncodeULEB128(5)); // length: 1 opcode + 4 address + bw.Write((byte)DwarfLineNumberExtendedOpcode.SetAddress); + bw.Write(address); + } + + /// + /// Writes an EndSequence extended opcode. + /// + private static void WriteEndSequence(BinaryWriter bw) + { + bw.Write((byte)0x00); + bw.Write(EncodeULEB128(1)); + bw.Write((byte)DwarfLineNumberExtendedOpcode.EndSequence); + } + + #endregion + + #region Error / Early-Return Paths + + [Fact] + public void ReadData_WithVersionLessThanTwo_ReturnsNull() + { + // Manually build data with version=1 so ReadData returns null. + var body = new MemoryStream(); + using (var bw = new BinaryWriter(body, System.Text.Encoding.UTF8, leaveOpen: true)) + { + bw.Write((ushort)1); // version < 2 + bw.Write(0); // header_length + bw.Write((byte)1); // minInstrLen + bw.Write((byte)1); // maxOpsPerInstr + bw.Write((byte)1); // default_is_stmt + bw.Write((byte)0); // lineBase + bw.Write((byte)1); // lineRange + bw.Write((byte)13); // opcodeBase + for (int i = 1; i < 13; i++) bw.Write((byte)0); + } + + byte[] bodyBytes = body.ToArray(); + byte[] debugLineData = BitConverter.GetBytes((uint)bodyBytes.Length) + .Concat(bodyBytes).Concat(new byte[] { 0x00 }).ToArray(); + + using var debugLine = new DwarfMemoryReader(debugLineData); + using var debugStrings = new DwarfMemoryReader(new byte[] { 0x00 }); + + var program = new DwarfLineNumberProgram(4, debugLine, debugStrings, addr => addr); + program.Files.Should().BeNull(); + } + + [Fact] + public void ReadData_WithOperationCodeBaseZero_ReturnsNull() + { + var body = new MemoryStream(); + using (var bw = new BinaryWriter(body, System.Text.Encoding.UTF8, leaveOpen: true)) + { + bw.Write((ushort)4); + bw.Write(0); + bw.Write((byte)1); + bw.Write((byte)1); // maxOpsPerInstr + bw.Write((byte)1); + bw.Write((byte)0); + bw.Write((byte)1); + bw.Write((byte)0); // operationCodeBase = 0 + } + + byte[] bodyBytes = body.ToArray(); + byte[] debugLineData = BitConverter.GetBytes((uint)bodyBytes.Length) + .Concat(bodyBytes).Concat(new byte[] { 0x00 }).ToArray(); + + using var debugLine = new DwarfMemoryReader(debugLineData); + using var debugStrings = new DwarfMemoryReader(new byte[] { 0x00 }); + + var program = new DwarfLineNumberProgram(4, debugLine, debugStrings, addr => addr); + program.Files.Should().BeNull(); + } + + [Fact] + public void ReadData_WithEndPositionBeyondBuffer_ReturnsNull() + { + // unitLength points past the end of data. + byte[] debugLineData = BitConverter.GetBytes((uint)9999) + .Concat(new byte[] { 0x04, 0x00 }) // version=4 stub + .ToArray(); + + using var debugLine = new DwarfMemoryReader(debugLineData); + using var debugStrings = new DwarfMemoryReader(new byte[] { 0x00 }); + + var program = new DwarfLineNumberProgram(4, debugLine, debugStrings, addr => addr); + program.Files.Should().BeNull(); + } + + #endregion + + #region Standard Opcodes + + [Fact] + public void ReadData_AdvancePc_AdvancesAddressByOperandTimesMinInstructionLength() + { + var program = BuildDwarf4Program(bw => + { + bw.Write((byte)DwarfLineNumberStandardOpcode.AdvancePc); + bw.Write(EncodeULEB128(5)); + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); + }, minimumInstructionLength: 4); + + program.Files.Should().HaveCount(1); + program.Files[0].Lines.Should().HaveCount(1); + program.Files[0].Lines[0].Address.Should().Be(20u); + } + + [Fact] + public void ReadData_AdvanceLine_PositiveValue_IncrementsLine() + { + var program = BuildDwarf4Program(bw => + { + bw.Write((byte)DwarfLineNumberStandardOpcode.AdvanceLine); + bw.Write(EncodeSLEB128(10)); + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); + }); + + program.Files[0].Lines.Should().HaveCount(1); + program.Files[0].Lines[0].Line.Should().Be(11u); // initial 1 + 10 + } + + [Fact] + public void ReadData_AdvanceLine_NegativeAfterPositive_DecrementsLine() + { + var program = BuildDwarf4Program(bw => + { + bw.Write((byte)DwarfLineNumberStandardOpcode.AdvanceLine); + bw.Write(EncodeSLEB128(20)); + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); + bw.Write((byte)DwarfLineNumberStandardOpcode.AdvanceLine); + bw.Write(EncodeSLEB128(-5)); + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); + }); + + program.Files[0].Lines.Should().HaveCount(2); + program.Files[0].Lines[0].Line.Should().Be(21u); + program.Files[0].Lines[1].Line.Should().Be(16u); + } + + [Fact] + public void ReadData_SetFile_SwitchesToSecondFile() + { + var program = BuildDwarf4Program(bw => + { + // SetFile to file 2 (1-based index) + bw.Write((byte)DwarfLineNumberStandardOpcode.SetFile); + bw.Write(EncodeULEB128(2)); + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); + }, fileNames: new[] { "first.c", "second.c" }); + + program.Files.Should().HaveCount(2); + program.Files[0].Lines.Should().BeEmpty(); + program.Files[1].Lines.Should().HaveCount(1); + program.Files[1].Name.Should().Be("second.c"); + } + + [Fact] + public void ReadData_SetColumn_SetsColumnOnEmittedLine() + { + var program = BuildDwarf4Program(bw => + { + bw.Write((byte)DwarfLineNumberStandardOpcode.SetColumn); + bw.Write(EncodeULEB128(42)); + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); + }); + + program.Files[0].Lines[0].Column.Should().Be(42ul); + } + + [Fact] + public void ReadData_ConstAddPc_AdvancesAddressByFormula() + { + // With opcodeBase=13, lineRange=14, minInstrLen=1: + // advance = (255 - 13) / 14 = 17 + var program = BuildDwarf4Program(bw => + { + bw.Write((byte)DwarfLineNumberStandardOpcode.ConstAddPc); + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); + }, lineRange: 14, operationCodeBase: 13); + + program.Files[0].Lines[0].Address.Should().Be(17u); + } + + [Fact] + public void ReadData_FixedAdvancePc_AddsUshortToAddress() + { + var program = BuildDwarf4Program(bw => + { + bw.Write((byte)DwarfLineNumberStandardOpcode.FixedAdvancePc); + bw.Write((ushort)256); + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); + }); + + program.Files[0].Lines[0].Address.Should().Be(256u); + } + + #endregion + + #region Extended Opcodes + + [Fact] + public void ReadData_SetAddress_SetsAddressToValue() + { + var program = BuildDwarf4Program(bw => + { + WriteSetAddress(bw, 0x2000); + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); + }); + + program.Files[0].Lines[0].Address.Should().Be(0x2000u); + } + + [Fact] + public void ReadData_EndSequence_EmitsLineAndResetsState() + { + var program = BuildDwarf4Program(bw => + { + bw.Write((byte)DwarfLineNumberStandardOpcode.AdvanceLine); + bw.Write(EncodeSLEB128(5)); + WriteSetAddress(bw, 0x1000); + WriteEndSequence(bw); + // After reset: address=0, line=1 + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); + }); + + program.Files[0].Lines.Should().HaveCount(2); + // EndSequence emission + program.Files[0].Lines[0].Address.Should().Be(0x1000u); + program.Files[0].Lines[0].Line.Should().Be(6u); // 1 + 5 + // After reset + program.Files[0].Lines[1].Address.Should().Be(0u); + program.Files[0].Lines[1].Line.Should().Be(1u); + } + + [Fact] + public void ReadData_SetAddress_WhenZero_UsesLastAddressFromPreviousEndSequence() + { + var program = BuildDwarf4Program(bw => + { + WriteSetAddress(bw, 0x5000); + WriteEndSequence(bw); + // SetAddress(0) should fall back to lastAddress = 0x5000 + WriteSetAddress(bw, 0); + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); + }); + + program.Files[0].Lines.Should().HaveCount(2); + program.Files[0].Lines[1].Address.Should().Be(0x5000u); + } + + [Fact] + public void ReadData_DefineFile_AddsAndSwitchesToNewFile() + { + var program = BuildDwarf4Program(bw => + { + // Extended: DefineFile + bw.Write((byte)0x00); // extended marker + byte[] nameBytes = System.Text.Encoding.UTF8.GetBytes("new.c"); + // length = 1 (opcode) + name.Length + 1 (null) + 3 (ULEB128 zeros) + int extLen = 1 + nameBytes.Length + 1 + 3; + bw.Write(EncodeULEB128((ulong)extLen)); + bw.Write((byte)DwarfLineNumberExtendedOpcode.DefineFile); + bw.Write(nameBytes); + bw.Write((byte)0x00); // null terminator + bw.Write((byte)0x00); // directory index + bw.Write((byte)0x00); // timestamp + bw.Write((byte)0x00); // length + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); + }); + + program.Files.Should().HaveCount(2); + program.Files[1].Name.Should().Be("new.c"); + program.Files[1].Lines.Should().HaveCount(1); + } + + #endregion + + #region Special Opcodes + + [Fact] + public void ReadData_SpecialOpcode_AdvancesAddressAndLine() + { + // lineBase=-3, lineRange=12, opcodeBase=13, minInstrLen=1 + // Opcode 20: adjusted = 20 - 13 = 7 + // operationAdvance = 7 / 12 = 0 + // lineAdvance = -3 + (7 % 12) = -3 + 7 = 4 + // Result: address=0, line=1+4=5 + var program = BuildDwarf4Program(bw => + { + bw.Write((byte)20); // special opcode + }, lineBase: -3, lineRange: 12); + + program.Files[0].Lines.Should().HaveCount(1); + program.Files[0].Lines[0].Address.Should().Be(0u); + program.Files[0].Lines[0].Line.Should().Be(5u); + } + + [Fact] + public void ReadData_SpecialOpcode_WithAddressAdvance() + { + // lineBase=0, lineRange=4, opcodeBase=13, minInstrLen=2 + // Opcode 21: adjusted = 21 - 13 = 8 + // operationAdvance = 8 / 4 = 2 + // lineAdvance = 0 + (8 % 4) = 0 + // address = 2 * 2 = 4, line = 1 + var program = BuildDwarf4Program(bw => + { + bw.Write((byte)21); + }, lineBase: 0, lineRange: 4, minimumInstructionLength: 2); + + program.Files[0].Lines[0].Address.Should().Be(4u); + program.Files[0].Lines[0].Line.Should().Be(1u); + } + + [Fact] + public void ReadData_MultipleSpecialOpcodes_AccumulateState() + { + // lineBase=0, lineRange=4, opcodeBase=13, minInstrLen=1 + // Opcode 17: adjusted=4, opAdvance=1, lineAdv=0. addr=1, line=1. + // Opcode 17: adjusted=4, opAdvance=1, lineAdv=0. addr=2, line=1. + var program = BuildDwarf4Program(bw => + { + bw.Write((byte)17); + bw.Write((byte)17); + }, lineBase: 0, lineRange: 4); + + program.Files[0].Lines.Should().HaveCount(2); + program.Files[0].Lines[0].Address.Should().Be(1u); + program.Files[0].Lines[1].Address.Should().Be(2u); + } + + #endregion + + #region Multiple Files, Directories, Normalization + + [Fact] + public void ReadData_Dwarf4_MultipleDirectories_ResolvesFilePaths() + { + var program = BuildDwarf4Program(bw => + { + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); + }, fileNames: null, directories: new[] { "src", "lib" }); + + // Override: need custom file entries with directory indices. + // Since BuildDwarf4Program doesn't support dir indices on files, + // build manually. + var body = new MemoryStream(); + using (var bw = new BinaryWriter(body, System.Text.Encoding.UTF8, leaveOpen: true)) + { + bw.Write((ushort)4); + bw.Write(0); + bw.Write((byte)1); + bw.Write((byte)1); + bw.Write((byte)1); + bw.Write((byte)0); + bw.Write((byte)1); + bw.Write((byte)13); + for (int i = 1; i < 13; i++) bw.Write((byte)0); + + // Directories + bw.Write(System.Text.Encoding.UTF8.GetBytes("src")); + bw.Write((byte)0x00); + bw.Write(System.Text.Encoding.UTF8.GetBytes("lib")); + bw.Write((byte)0x00); + bw.Write((byte)0x00); // terminator + + // File in dir 1 (src) + bw.Write(System.Text.Encoding.UTF8.GetBytes("main.c")); + bw.Write((byte)0x00); + bw.Write(EncodeULEB128(1)); // directory index 1 = "src" + bw.Write((byte)0x00); + bw.Write((byte)0x00); + + // File in dir 2 (lib) + bw.Write(System.Text.Encoding.UTF8.GetBytes("util.c")); + bw.Write((byte)0x00); + bw.Write(EncodeULEB128(2)); // directory index 2 = "lib" + bw.Write((byte)0x00); + bw.Write((byte)0x00); + + bw.Write((byte)0x00); // file terminator + + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); + } + + byte[] bodyBytes = body.ToArray(); + byte[] debugLineData = BitConverter.GetBytes((uint)bodyBytes.Length) + .Concat(bodyBytes).Concat(new byte[] { 0x00 }).ToArray(); + + using var debugLine = new DwarfMemoryReader(debugLineData); + using var debugStrings = new DwarfMemoryReader(new byte[] { 0x00 }); + + var prog = new DwarfLineNumberProgram(4, debugLine, debugStrings, addr => addr); + + prog.Files.Should().HaveCount(2); + prog.Files[0].Path.Should().Contain("src"); + prog.Files[0].Path.Should().Contain("main.c"); + prog.Files[1].Path.Should().Contain("lib"); + prog.Files[1].Path.Should().Contain("util.c"); + } + + [Fact] + public void ReadData_Dwarf4_MultipleFiles_LinesAssignedToCorrectFile() + { + var program = BuildDwarf4Program(bw => + { + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); // line on file 1 + bw.Write((byte)DwarfLineNumberStandardOpcode.SetFile); + bw.Write(EncodeULEB128(2)); // switch to file 2 + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); // line on file 2 + }, fileNames: new[] { "a.c", "b.c" }); + + program.Files[0].Lines.Should().HaveCount(1); + program.Files[1].Lines.Should().HaveCount(1); + } + + [Fact] + public void ReadData_NormalizationAppliedToAllLinesAcrossFiles() + { + var program = BuildDwarf4Program(bw => + { + WriteSetAddress(bw, 0x100); + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); + bw.Write((byte)DwarfLineNumberStandardOpcode.SetFile); + bw.Write(EncodeULEB128(2)); + WriteSetAddress(bw, 0x200); + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); + }, fileNames: new[] { "a.c", "b.c" }, + addressNormalizer: addr => addr + 0x1000); + + program.Files[0].Lines[0].Address.Should().Be(0x1100u); + program.Files[1].Lines[0].Address.Should().Be(0x1200u); + } + + [Fact] + public void ReadData_Dwarf3_ParsesWithoutMaxOperationsPerInstruction() + { + // DWARF3: no maxOperationsPerInstruction field in header. + var program = BuildDwarf4Program(bw => + { + bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); + }, dwarfVersion: 3); + + program.Files.Should().NotBeNull(); + program.Files.Should().HaveCount(1); + program.Files[0].Lines.Should().HaveCount(1); + } + + #endregion } } From 531e04a3abb2af05edd39642e1a1cfc4c662456d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20=C5=A0vendov=C3=A1?= Date: Fri, 24 Apr 2026 11:12:10 +0200 Subject: [PATCH 08/36] Add integration tests for BinSkim with BinSkimRunner helper class - Created Test.IntegrationTests.BinSkim.Driver project and added to solution - Implemented BinSkimRunner to launch BinSkim as an external process - Added AnalyzeCommandIntegrationTests with 5 passing tests for self-scan and command line options --- .memory-bank/activeContext.md | 86 ++++------- src/BinSkim.sln | 6 + .../AnalyzeCommandIntegrationTests.cs | 114 ++++++++++++++ .../BinSkimRunner.cs | 144 ++++++++++++++++++ ...est.IntegrationTests.BinSkim.Driver.csproj | 27 ++++ 5 files changed, 321 insertions(+), 56 deletions(-) create mode 100644 src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs create mode 100644 src/Test.IntegrationTests.BinSkim.Driver/BinSkimRunner.cs create mode 100644 src/Test.IntegrationTests.BinSkim.Driver/Test.IntegrationTests.BinSkim.Driver.csproj diff --git a/.memory-bank/activeContext.md b/.memory-bank/activeContext.md index 4216bdcd..e444b6a4 100644 --- a/.memory-bank/activeContext.md +++ b/.memory-bank/activeContext.md @@ -1,65 +1,39 @@ # Active Context ## Current Work Focus -Planning guardian integration tests for BinSkim. +BinSkim Integration Tests - Track 1 complete, Tracks 2-4 ready. ## Recent Changes -- (none - new work focus) +- ✅ Created Test.IntegrationTests.BinSkim.Driver project (xUnit, net9.0, FluentAssertions, Sarif.Driver) +- ✅ Added project to BinSkim.sln +- ✅ Created BinSkimRunner helper class (launches BinSkim via dotnet BinSkim.dll, captures stdout/stderr/exit code, optional SARIF deserialization) +- ✅ Created 5 passing integration tests in AnalyzeCommandIntegrationTests +- ✅ Build: 0 warnings, 0 errors; Tests: 5/5 passing ## Active Decisions -- Pending: scope clarification needed before plan creation (see Open Questions below) - -## Research Findings - -### What Guardian Means in This Repo -- Guardian = Microsoft 1ES SDL infrastructure pools (guardian-build-infra-*) -- 1ESPT = 1 Engineering System Perf and Test pool for compliance scanning -- NOT a NuGet package, NOT a separate tool - it is infrastructure -- No .gdnconfig or .gdn files exist in the repo - -### Existing Pipelines -- .pipelines/1ES.binskim.BuildAndTest.EXTERNAL.yml - build/test pipeline on guardian-build-infra pools (Linux + Windows matrix), triggers internal pipeline ID 21327 -- .pipelines/1ES.binskim.compliance.EXTERNAL.yml - compliance/SDL pipeline on pool-1espt-mseng, runs BinSkim v4.3.1, CodeQL, CredScan, SPMI on built outputs -- Build uses 1ES.Official.PipelineTemplate.yml, compliance uses 1ES.Unofficial.PipelineTemplate.yml - -### Compliance Pipeline Details (1ES.binskim.compliance.EXTERNAL.yml) -- Trigger: main branch only -- Builds with BuildAndTest.cmd, then runs BinSkim@4 task (exact version 4.3.1) -- AnalyzeTargetGlob scans net9.0 release output DLLs and EXEs -- AnalyzeSymPath uses Cache + symweb -- Also runs CredScan@2, publishes security analysis logs - -### Build Pipeline Details (1ES.binskim.BuildAndTest.EXTERNAL.yml) -- Trigger: every PR and commit -- Matrix: Linux (guardian-build-infra-linux-x64) and Windows (guardian-build-infra-windows-x64) -- Uses custom NuGet feed: BinSkim.Build on mseng/1ES -- Has ValidateReleasePRs stage (checks PRs in ReleaseHistory.md are merged) -- Has InternalValidation stage (triggers internal BinSkimInternal pipeline ID 21327, polls for completion) - -### Existing Test Patterns -- Test.FunctionalTests.BinSkim.Driver - end-to-end driver/baseline tests with expected SARIF outputs -- Test.FunctionalTests.BinSkim.Rules - rule functional tests with Pass/Fail test data folders -- Test.UnitTests.* - unit test projects -- Tech stack: xUnit, FluentAssertions, .NET 9 -- Test assets include PE, ELF, Mach-O binaries with various compiler versions -- Baseline tests compare actual SARIF output against Expected/ and NonWindowsExpected/ folders - -### Current State - No Guardian Integration Tests Exist -- No dedicated guardian/1ESPT integration test suite -- No cross-project validation tests -- BinSkim self-scans in compliance pipeline but no formal integration test suite - -## Open Questions (Asked, Awaiting Response) -Scope clarification: What does "guardian integration tests" mean? -- Option A: New ADO pipeline YAML that runs BinSkim on curated test binaries on 1ESPT infrastructure, validates SARIF/SDL reporting end-to-end -- Option B: Local xUnit integration tests that invoke BinSkim as external process with same args as compliance pipeline, validate SARIF output - runnable without 1ESPT -- Option C: Both pipeline-based + local integration tests -- Option D: Something else - -## Next Steps -1. Get scope clarification from user (Options A/B/C/D above) -2. Create detailed implementation plan based on answer -3. Decompose into tracks and tasks per CoDev workflow +- Invocation: dotnet BinSkim.dll for cross-platform (Linux + Windows) +- BinSkim.Driver referenced with ReferenceOutputAssembly=false (build-order dependency only) +- Path resolution: navigate from test assembly up to ld/bin/BinSkim.Driver/release/BinSkim.dll +- Self-scan pattern: BinSkim analyzing its own DLL (PDB co-located) +- CommandLineParser writes help/version to stderr, tests check combined output + +## Files Created/Modified +- NEW: src/Test.IntegrationTests.BinSkim.Driver/Test.IntegrationTests.BinSkim.Driver.csproj +- NEW: src/Test.IntegrationTests.BinSkim.Driver/BinSkimRunner.cs +- NEW: src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs +- MOD: src/BinSkim.sln (added project + build configs) + +## Test Inventory (5 tests, all passing) +1. Analyze_SelfScan_ExitsWithZero - Scans BinSkim.dll, expects exit 0 +2. Analyze_SelfScan_ProducesValidSarif - Validates SARIF structure and tool name +3. Analyze_NoValidTargets_ExitsWithNonZero - Non-existent target, expects non-zero exit +4. Analyze_HelpFlag_ExitsCleanly - help verb, exit 0, output present +5. Analyze_VersionFlag_ExitsCleanly - --version flag, exit 0, output present + +## Next Steps (Tracks 2-4 from plan) +1. ☐ Track 2: Core Integration Tests - exit codes, analyze/dump/export verbs, known-bad binary scanning +2. ☐ Track 3: CLI Behavior Tests - response files, --recurse, config files, rule selection, error handling +3. ☐ Track 4: SARIF Validation Tests - schema compliance, determinism, data insertion/removal ## Current State -Research complete. Awaiting user decision on scope before creating implementation plan. +Track 1 complete. Integration test project scaffolding + BinSkimRunner helper + 5 tests all green. diff --git a/src/BinSkim.sln b/src/BinSkim.sln index d4321865..aa6a91ec 100644 --- a/src/BinSkim.sln +++ b/src/BinSkim.sln @@ -42,6 +42,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Test.UnitTests.BinSkim.Rule EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Test.UnitTests.BinSkim.Driver", "Test.UnitTests.BinSkim.Driver\Test.UnitTests.BinSkim.Driver.csproj", "{B64DBE60-C7E6-48C1-BB7F-B12129DF98B2}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Test.IntegrationTests.BinSkim.Driver", "Test.IntegrationTests.BinSkim.Driver\Test.IntegrationTests.BinSkim.Driver.csproj", "{2E4F8A1B-3C5D-4E6F-9A0B-1C2D3E4F5A6B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -84,6 +86,10 @@ Global {B64DBE60-C7E6-48C1-BB7F-B12129DF98B2}.Debug|Any CPU.Build.0 = Debug|AnyCPU {B64DBE60-C7E6-48C1-BB7F-B12129DF98B2}.Release|Any CPU.ActiveCfg = Release|AnyCPU {B64DBE60-C7E6-48C1-BB7F-B12129DF98B2}.Release|Any CPU.Build.0 = Release|AnyCPU + {2E4F8A1B-3C5D-4E6F-9A0B-1C2D3E4F5A6B}.Debug|Any CPU.ActiveCfg = Debug|AnyCPU + {2E4F8A1B-3C5D-4E6F-9A0B-1C2D3E4F5A6B}.Debug|Any CPU.Build.0 = Debug|AnyCPU + {2E4F8A1B-3C5D-4E6F-9A0B-1C2D3E4F5A6B}.Release|Any CPU.ActiveCfg = Release|AnyCPU + {2E4F8A1B-3C5D-4E6F-9A0B-1C2D3E4F5A6B}.Release|Any CPU.Build.0 = Release|AnyCPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs b/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs new file mode 100644 index 00000000..8a7636d0 --- /dev/null +++ b/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Threading.Tasks; + +using FluentAssertions; + +using Microsoft.CodeAnalysis.Sarif; + +using Xunit; + +namespace Microsoft.CodeAnalysis.IL +{ + [Trait("Category", "Integration")] + public class AnalyzeCommandIntegrationTests : IDisposable + { + private readonly string _tempDir; + + public AnalyzeCommandIntegrationTests() + { + _tempDir = Path.Combine(Path.GetTempPath(), "BinSkimIntegration", Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(_tempDir); + } + + public void Dispose() + { + try { Directory.Delete(_tempDir, recursive: true); } catch { /* best-effort cleanup */ } + } + + [Fact] + public async Task Analyze_SelfScan_ExitsWithZero() + { + // BinSkim analyzing its own DLL — PDB is co-located so symbol loading should succeed. + string targetBinary = BinSkimRunner.GetBinSkimDllPath(); + string sarifOutput = Path.Combine(_tempDir, "output.sarif"); + + BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] + { + "analyze", + targetBinary, + "-o", sarifOutput, + }); + + result.ExitCode.Should().Be(0, + $"BinSkim should exit cleanly.\nStdOut: {result.StdOut}\nStdErr: {result.StdErr}"); + } + + [Fact] + public async Task Analyze_SelfScan_ProducesValidSarif() + { + string targetBinary = BinSkimRunner.GetBinSkimDllPath(); + string sarifOutput = Path.Combine(_tempDir, "output.sarif"); + + BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] + { + "analyze", + targetBinary, + "-o", sarifOutput, + }); + + File.Exists(sarifOutput).Should().BeTrue("SARIF output file should be created"); + + SarifLog sarifLog = result.LoadSarifLog(); + sarifLog.Should().NotBeNull(); + sarifLog.Runs.Should().HaveCount(1); + sarifLog.Runs[0].Tool.Driver.Name.Should().Be("BinSkim"); + } + + [Fact] + public async Task Analyze_NoValidTargets_ExitsWithNonZero() + { + string nonExistentTarget = Path.Combine(_tempDir, "does_not_exist.dll"); + string sarifOutput = Path.Combine(_tempDir, "output.sarif"); + + BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] + { + "analyze", + nonExistentTarget, + "-o", sarifOutput, + }); + + result.ExitCode.Should().NotBe(0, + "BinSkim should report a non-zero exit code when no valid targets are found."); + } + + [Fact] + public async Task Analyze_HelpFlag_ExitsCleanly() + { + BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] + { + "help", + }); + + result.ExitCode.Should().Be(0); + string combinedOutput = result.StdOut + result.StdErr; + combinedOutput.Should().NotBeNullOrWhiteSpace("help output should be printed"); + } + + [Fact] + public async Task Analyze_VersionFlag_ExitsCleanly() + { + BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] + { + "--version", + }); + + result.ExitCode.Should().Be(0); + string combinedOutput = result.StdOut + result.StdErr; + combinedOutput.Should().NotBeNullOrWhiteSpace("version output should be printed"); + } + } +} diff --git a/src/Test.IntegrationTests.BinSkim.Driver/BinSkimRunner.cs b/src/Test.IntegrationTests.BinSkim.Driver/BinSkimRunner.cs new file mode 100644 index 00000000..10559e79 --- /dev/null +++ b/src/Test.IntegrationTests.BinSkim.Driver/BinSkimRunner.cs @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.CodeAnalysis.Sarif; + +using Newtonsoft.Json; + +namespace Microsoft.CodeAnalysis.IL +{ + /// + /// Result of running BinSkim as an external process. + /// + public sealed class BinSkimRunResult + { + public int ExitCode { get; init; } + public string StdOut { get; init; } + public string StdErr { get; init; } + public string SarifOutputPath { get; init; } + + /// + /// Deserializes the SARIF output file if it exists. + /// Returns null if no output path was specified or the file was not created. + /// + public SarifLog LoadSarifLog() + { + if (string.IsNullOrEmpty(SarifOutputPath) || !File.Exists(SarifOutputPath)) + { + return null; + } + + string json = File.ReadAllText(SarifOutputPath); + return JsonConvert.DeserializeObject(json); + } + } + + /// + /// Launches BinSkim as an external process via "dotnet BinSkim.dll" for cross-platform compatibility. + /// Captures stdout, stderr, exit code, and optionally deserializes produced SARIF output. + /// + public static class BinSkimRunner + { + private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(120); + + /// + /// Resolves the path to the built BinSkim.dll relative to the test assembly location. + /// Layout: bld/bin/Test.IntegrationTests.BinSkim.Driver/release/ → ../../BinSkim.Driver/release/BinSkim.dll + /// + public static string GetBinSkimDllPath() + { + string assemblyDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + string binSkimDll = Path.GetFullPath( + Path.Combine(assemblyDir, "..", "..", "BinSkim.Driver", "release", "BinSkim.dll")); + + if (!File.Exists(binSkimDll)) + { + throw new FileNotFoundException( + $"BinSkim.dll not found at expected path: {binSkimDll}. " + + "Ensure the BinSkim.Driver project has been built in Release configuration."); + } + + return binSkimDll; + } + + /// + /// Runs BinSkim as an external process with the specified arguments. + /// + public static async Task RunAsync( + string[] args, + TimeSpan? timeout = null) + { + string binSkimDll = GetBinSkimDllPath(); + timeout ??= DefaultTimeout; + + var startInfo = new ProcessStartInfo + { + FileName = "dotnet", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }; + + startInfo.ArgumentList.Add(binSkimDll); + foreach (string arg in args) + { + startInfo.ArgumentList.Add(arg); + } + + using var process = new Process { StartInfo = startInfo }; + using var cts = new CancellationTokenSource(timeout.Value); + + process.Start(); + + // Read stdout and stderr concurrently to avoid deadlocks from filled OS buffers. + Task stdOutTask = process.StandardOutput.ReadToEndAsync(); + Task stdErrTask = process.StandardError.ReadToEndAsync(); + + try + { + await process.WaitForExitAsync(cts.Token); + } + catch (OperationCanceledException) + { + try { process.Kill(entireProcessTree: true); } catch { /* best-effort cleanup */ } + throw new TimeoutException( + $"BinSkim process did not exit within {timeout.Value.TotalSeconds}s. " + + $"Args: {string.Join(" ", args)}"); + } + + string stdOut = await stdOutTask; + string stdErr = await stdErrTask; + + string sarifPath = FindOutputPath(args); + + return new BinSkimRunResult + { + ExitCode = process.ExitCode, + StdOut = stdOut, + StdErr = stdErr, + SarifOutputPath = sarifPath, + }; + } + + private static string FindOutputPath(string[] args) + { + for (int i = 0; i < args.Length - 1; i++) + { + if (args[i] is "--output" or "-o") + { + return args[i + 1]; + } + } + + return null; + } + } +} diff --git a/src/Test.IntegrationTests.BinSkim.Driver/Test.IntegrationTests.BinSkim.Driver.csproj b/src/Test.IntegrationTests.BinSkim.Driver/Test.IntegrationTests.BinSkim.Driver.csproj new file mode 100644 index 00000000..1e836d59 --- /dev/null +++ b/src/Test.IntegrationTests.BinSkim.Driver/Test.IntegrationTests.BinSkim.Driver.csproj @@ -0,0 +1,27 @@ + + + + + $(NetCoreVersion) + Library + True + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + false + false + + + From 9d2e225ce31a6116fb322e087d5f5735ef90b929 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20=C5=A0vendov=C3=A1?= Date: Fri, 24 Apr 2026 11:24:10 +0200 Subject: [PATCH 09/36] Add integration tests for new Dwarf commands and enhance existing tests --- .memory-bank/activeContext.md | 47 +++-- .../AnalyzeCommandIntegrationTests.cs | 188 ++++++++++++++++++ 2 files changed, 217 insertions(+), 18 deletions(-) diff --git a/.memory-bank/activeContext.md b/.memory-bank/activeContext.md index e444b6a4..d05733fa 100644 --- a/.memory-bank/activeContext.md +++ b/.memory-bank/activeContext.md @@ -1,21 +1,22 @@ # Active Context ## Current Work Focus -BinSkim Integration Tests - Track 1 complete, Tracks 2-4 ready. +BinSkim Integration Tests - Tracks 1-2 complete, Tracks 3-4 ready. ## Recent Changes -- ✅ Created Test.IntegrationTests.BinSkim.Driver project (xUnit, net9.0, FluentAssertions, Sarif.Driver) -- ✅ Added project to BinSkim.sln -- ✅ Created BinSkimRunner helper class (launches BinSkim via dotnet BinSkim.dll, captures stdout/stderr/exit code, optional SARIF deserialization) -- ✅ Created 5 passing integration tests in AnalyzeCommandIntegrationTests -- ✅ Build: 0 warnings, 0 errors; Tests: 5/5 passing +- ✅ Track 1: Project scaffolding, BinSkimRunner helper, 5 initial tests +- ✅ Track 2: Core integration tests - 8 new tests (analyze verbs, dump, export-rules, export-config, error handling) +- Build: 0 warnings, 0 errors; Tests: 13/13 passing ## Active Decisions - Invocation: dotnet BinSkim.dll for cross-platform (Linux + Windows) - BinSkim.Driver referenced with ReferenceOutputAssembly=false (build-order dependency only) -- Path resolution: navigate from test assembly up to ld/bin/BinSkim.Driver/release/BinSkim.dll +- Path resolution: navigate from test assembly up to bld/bin/BinSkim.Driver/release/BinSkim.dll - Self-scan pattern: BinSkim analyzing its own DLL (PDB co-located) - CommandLineParser writes help/version to stderr, tests check combined output +- BinSkim exits 0 even when rules fire errors. Exit code reflects tool health, not rule results +- Verb names: analyze, dump, export-rules, export-config (NOT export-rules-metadata/export-configuration) +- export-rules/export-config take positional output path arg (not --output) ## Files Created/Modified - NEW: src/Test.IntegrationTests.BinSkim.Driver/Test.IntegrationTests.BinSkim.Driver.csproj @@ -23,17 +24,27 @@ BinSkim Integration Tests - Track 1 complete, Tracks 2-4 ready. - NEW: src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs - MOD: src/BinSkim.sln (added project + build configs) -## Test Inventory (5 tests, all passing) -1. Analyze_SelfScan_ExitsWithZero - Scans BinSkim.dll, expects exit 0 -2. Analyze_SelfScan_ProducesValidSarif - Validates SARIF structure and tool name -3. Analyze_NoValidTargets_ExitsWithNonZero - Non-existent target, expects non-zero exit -4. Analyze_HelpFlag_ExitsCleanly - help verb, exit 0, output present -5. Analyze_VersionFlag_ExitsCleanly - --version flag, exit 0, output present +## Test Inventory (13 tests, all passing) +### Track 1 (original) +1. Analyze_SelfScan_ExitsWithZero +2. Analyze_SelfScan_ProducesValidSarif +3. Analyze_NoValidTargets_ExitsWithNonZero +4. Analyze_HelpFlag_ExitsCleanly +5. Analyze_VersionFlag_ExitsCleanly -## Next Steps (Tracks 2-4 from plan) -1. ☐ Track 2: Core Integration Tests - exit codes, analyze/dump/export verbs, known-bad binary scanning -2. ☐ Track 3: CLI Behavior Tests - response files, --recurse, config files, rule selection, error handling -3. ☐ Track 4: SARIF Validation Tests - schema compliance, determinism, data insertion/removal +### Track 2 (new) +6. Analyze_KnownFailBinary_ProducesErrorResults - BA2016 fires error on ManagedFail.dll +7. Analyze_RunOnlyRules_FiltersToSpecifiedRule - --run-only-rules BA2016 filters results +8. Analyze_InvalidArgument_ExitsWithNonZero - --bogus-flag gives non-zero +9. Analyze_InvalidVerb_ExitsWithNonZero - unrecognized verb gives non-zero +10. Dump_SelfScan_ProducesMetadataOutput - dump outputs binary metadata +11. Dump_Verbose_ProducesMoreDetailedOutput - --verbose produces >= normal output +12. ExportRules_ProducesValidSarifOutput - export-rules creates .sarif with rule BA2016 +13. ExportConfig_ProducesValidJsonOutput - export-config creates .json config + +## Next Steps (Tracks 3-4 from plan) +1. ☐ Track 3: CLI Behavior Tests - response files, --recurse, config files, error handling +2. ☐ Track 4: SARIF Validation Tests - schema compliance, determinism, data insertion/removal ## Current State -Track 1 complete. Integration test project scaffolding + BinSkimRunner helper + 5 tests all green. +Tracks 1-2 complete. 13 integration tests all green. Ready for Tracks 3-4. diff --git a/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs b/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs index 8a7636d0..0568e7c9 100644 --- a/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs +++ b/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs @@ -3,6 +3,8 @@ using System; using System.IO; +using System.Linq; +using System.Reflection; using System.Threading.Tasks; using FluentAssertions; @@ -68,6 +70,63 @@ public async Task Analyze_SelfScan_ProducesValidSarif() sarifLog.Runs[0].Tool.Driver.Name.Should().Be("BinSkim"); } + [Fact] + public async Task Analyze_KnownFailBinary_ProducesErrorResults() + { + string failBinary = GetFunctionalTestDataPath( + "BA2016.MarkImageAsNXCompatible", "Fail", "ManagedFail.dll"); + string sarifOutput = Path.Combine(_tempDir, "fail-output.sarif"); + + BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] + { + "analyze", + failBinary, + "-o", sarifOutput, + "--run-only-rules", "BA2016", + "--kind", "Fail", + "--level", "Error;Warning;Note", + }); + + // BinSkim exits 0 even when rules fire errors — exit code reflects tool health, not rule results. + result.ExitCode.Should().Be(0, + $"BinSkim should complete analysis successfully.\nStdErr: {result.StdErr}"); + + SarifLog sarifLog = result.LoadSarifLog(); + sarifLog.Should().NotBeNull(); + sarifLog.Runs[0].Results.Should().Contain( + r => r.RuleId == "BA2016" && r.Level == FailureLevel.Error, + "BA2016 should fire an error for ManagedFail.dll (not NX compatible)"); + } + + [Fact] + public async Task Analyze_RunOnlyRules_FiltersToSpecifiedRule() + { + string targetBinary = BinSkimRunner.GetBinSkimDllPath(); + string sarifOutput = Path.Combine(_tempDir, "filtered.sarif"); + + BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] + { + "analyze", + targetBinary, + "-o", sarifOutput, + "--run-only-rules", "BA2016", + "--kind", "Fail;Pass;NotApplicable", + "--level", "Error;Warning;Note", + }); + + result.ExitCode.Should().Be(0); + + SarifLog sarifLog = result.LoadSarifLog(); + sarifLog.Should().NotBeNull(); + + // Only BA2016 results should appear (plus WRN999 notifications for disabled rules). + sarifLog.Runs[0].Results + .Where(r => !r.RuleId.StartsWith("WRN")) + .Should().OnlyContain( + r => r.RuleId == "BA2016", + "only BA2016 results should be present when --run-only-rules BA2016 is specified"); + } + [Fact] public async Task Analyze_NoValidTargets_ExitsWithNonZero() { @@ -85,6 +144,31 @@ public async Task Analyze_NoValidTargets_ExitsWithNonZero() "BinSkim should report a non-zero exit code when no valid targets are found."); } + [Fact] + public async Task Analyze_InvalidArgument_ExitsWithNonZero() + { + BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] + { + "analyze", + "--bogus-flag", + }); + + result.ExitCode.Should().NotBe(0, + "An unrecognized CLI argument should produce a non-zero exit code."); + } + + [Fact] + public async Task Analyze_InvalidVerb_ExitsWithNonZero() + { + BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] + { + "not-a-real-verb", + }); + + result.ExitCode.Should().NotBe(0, + "An unrecognized verb should produce a non-zero exit code."); + } + [Fact] public async Task Analyze_HelpFlag_ExitsCleanly() { @@ -110,5 +194,109 @@ public async Task Analyze_VersionFlag_ExitsCleanly() string combinedOutput = result.StdOut + result.StdErr; combinedOutput.Should().NotBeNullOrWhiteSpace("version output should be printed"); } + + [Fact] + public async Task Dump_SelfScan_ProducesMetadataOutput() + { + string targetBinary = BinSkimRunner.GetBinSkimDllPath(); + + BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] + { + "dump", + targetBinary, + }); + + result.ExitCode.Should().Be(0, + $"Dump should succeed.\nStdErr: {result.StdErr}"); + result.StdOut.Should().Contain("BinSkim.dll", + "dump output should reference the target binary"); + } + + [Fact] + public async Task Dump_Verbose_ProducesMoreDetailedOutput() + { + string targetBinary = BinSkimRunner.GetBinSkimDllPath(); + + BinSkimRunResult normalResult = await BinSkimRunner.RunAsync(new[] + { + "dump", + targetBinary, + }); + + BinSkimRunResult verboseResult = await BinSkimRunner.RunAsync(new[] + { + "dump", + targetBinary, + "--verbose", + }); + + normalResult.ExitCode.Should().Be(0); + verboseResult.ExitCode.Should().Be(0); + + verboseResult.StdOut.Length.Should().BeGreaterThanOrEqualTo(normalResult.StdOut.Length, + "verbose dump should produce output at least as detailed as non-verbose"); + } + + [Fact] + public async Task ExportRules_ProducesValidSarifOutput() + { + string outputPath = Path.Combine(_tempDir, "rules.sarif"); + + BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] + { + "export-rules", + outputPath, + }); + + result.ExitCode.Should().Be(0, + $"export-rules should succeed.\nStdErr: {result.StdErr}"); + + File.Exists(outputPath).Should().BeTrue("rules SARIF file should be created"); + + string json = File.ReadAllText(outputPath); + json.Should().Contain("BA2016", + "exported rules should include BA2016 (MarkImageAsNXCompatible)"); + } + + [Fact] + public async Task ExportConfig_ProducesValidJsonOutput() + { + string outputPath = Path.Combine(_tempDir, "config.json"); + + BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] + { + "export-config", + outputPath, + }); + + result.ExitCode.Should().Be(0, + $"export-config should succeed.\nStdErr: {result.StdErr}"); + + File.Exists(outputPath).Should().BeTrue("config JSON file should be created"); + + string json = File.ReadAllText(outputPath); + json.Should().NotBeNullOrWhiteSpace("config file should contain content"); + } + + /// + /// Resolves a path into the BinSkim.Rules functional test data directory. + /// These binaries are curated per-rule with known Pass/Fail outcomes. + /// + private static string GetFunctionalTestDataPath(params string[] relativeParts) + { + string assemblyDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + string testDataRoot = Path.GetFullPath( + Path.Combine(assemblyDir, "..", "..", "..", "..", "src", + "Test.FunctionalTests.BinSkim.Rules", "FunctionalTestData")); + string fullPath = Path.Combine(new[] { testDataRoot }.Concat(relativeParts).ToArray()); + + if (!File.Exists(fullPath) && !Directory.Exists(fullPath)) + { + throw new FileNotFoundException( + $"Functional test data not found at: {fullPath}"); + } + + return fullPath; + } } } From 5b8b769f971a79ffd9d185f6f591b9a9e4e21d26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20=C5=A0vendov=C3=A1?= Date: Thu, 30 Apr 2026 10:04:01 +0200 Subject: [PATCH 10/36] acitve context --- .memory-bank/activeContext.md | 1 - src/BinaryParsers/VersionConstants.cs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.memory-bank/activeContext.md b/.memory-bank/activeContext.md index d05733fa..42430964 100644 --- a/.memory-bank/activeContext.md +++ b/.memory-bank/activeContext.md @@ -44,7 +44,6 @@ BinSkim Integration Tests - Tracks 1-2 complete, Tracks 3-4 ready. ## Next Steps (Tracks 3-4 from plan) 1. ☐ Track 3: CLI Behavior Tests - response files, --recurse, config files, error handling -2. ☐ Track 4: SARIF Validation Tests - schema compliance, determinism, data insertion/removal ## Current State Tracks 1-2 complete. 13 integration tests all green. Ready for Tracks 3-4. diff --git a/src/BinaryParsers/VersionConstants.cs b/src/BinaryParsers/VersionConstants.cs index fc195b32..7b2c30e7 100644 --- a/src/BinaryParsers/VersionConstants.cs +++ b/src/BinaryParsers/VersionConstants.cs @@ -4,7 +4,7 @@ namespace Microsoft.CodeAnalysis.IL { public static class VersionConstants { - public const string Prerelease = ".6"; + public const string Prerelease = ".7"; public const string AssemblyVersion = "4.4.9"; public const string FileVersion = "4.4.9"; public const string Version = AssemblyVersion + Prerelease; From 2db23b0a2cd49ecbf1b2ebdff1f599c4e71c3e70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20=C5=A0vendov=C3=A1?= Date: Thu, 28 May 2026 11:45:30 +0200 Subject: [PATCH 11/36] Fix directory index boundary check and update Strp offset in unit tests --- src/BinaryParsers/ElfBinary/Dwarf/DwarfLineNumberProgram.cs | 2 +- .../Dwarf/DwarfCompilationUnitFormTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/BinaryParsers/ElfBinary/Dwarf/DwarfLineNumberProgram.cs b/src/BinaryParsers/ElfBinary/Dwarf/DwarfLineNumberProgram.cs index aaf33c02..99a82c5c 100644 --- a/src/BinaryParsers/ElfBinary/Dwarf/DwarfLineNumberProgram.cs +++ b/src/BinaryParsers/ElfBinary/Dwarf/DwarfLineNumberProgram.cs @@ -657,7 +657,7 @@ private static DwarfFileInformation ReadFile(DwarfMemoryReader debugLine, List 0 && directoryIndex <= directories.Count - 1) ? directories[directoryIndex - 1] : null; + string directory = (directoryIndex > 0 && directoryIndex <= directories.Count) ? directories[directoryIndex - 1] : null; string path; path = string.IsNullOrEmpty(directory) || Path.IsPathRooted(name) ? name : Path.Combine(directory, name); diff --git a/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfCompilationUnitFormTests.cs b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfCompilationUnitFormTests.cs index 210f1314..a18441b9 100644 --- a/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfCompilationUnitFormTests.cs +++ b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfCompilationUnitFormTests.cs @@ -522,7 +522,7 @@ public void ReadData_Dwarf4_BlockAndStringForms_MapToBlockAndString() .Concat(blockLen) // Block length .Concat(blockPayload) // Block bytes .Concat(inlineString) // String - .Concat(new byte[] { 0x02, 0x00 }) // Strp: offset 2 into debug_str + .Concat(new byte[] { 0x02, 0x00, 0x00, 0x00 }) // Strp: 4-byte offset 2 into debug_str .ToArray(); // .debug_str: two bytes of padding, then "World\0" From 20ce7d5d80088c5005a09dfaee50cabfae506440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20=C5=A0vendov=C3=A1?= Date: Thu, 28 May 2026 13:45:37 +0200 Subject: [PATCH 12/36] Add unit tests for ElfBinary class to validate loading and error handling --- .github/03-feature.instructions.md | 4 +- .../Elf/ElfBinaryTests.cs | 258 ++++++++++++++++++ 2 files changed, 260 insertions(+), 2 deletions(-) diff --git a/.github/03-feature.instructions.md b/.github/03-feature.instructions.md index 2ba45b4d..13ef1d67 100644 --- a/.github/03-feature.instructions.md +++ b/.github/03-feature.instructions.md @@ -3,8 +3,7 @@ applyTo: "**" --- ## Feature Instructions - - Add unit tests to binskim class: +Add unit tests to binskim class: DwarfAttributeValue DwarfCommonInformationEntry DwarfCompilationUnit @@ -20,5 +19,6 @@ guardian pipeline - run guardian with binskim on 1ESPT Binskim Integration tests pester tests (powershell) Get rid of Verify Methods +do the tests less noisy - show only errors-warnings-exceptions? ## Testing Configuration diff --git a/src/Test.UnitTests.BinaryParsers/Elf/ElfBinaryTests.cs b/src/Test.UnitTests.BinaryParsers/Elf/ElfBinaryTests.cs index 59d3e3fe..85b4ad98 100644 --- a/src/Test.UnitTests.BinaryParsers/Elf/ElfBinaryTests.cs +++ b/src/Test.UnitTests.BinaryParsers/Elf/ElfBinaryTests.cs @@ -7,6 +7,8 @@ using System.Linq; using System.Reflection; +using ELFSharp.ELF.Segments; + using FluentAssertions; using Microsoft.CodeAnalysis.BinaryParsers.Dwarf; @@ -294,5 +296,261 @@ public void ValidateDwarfV4_WithO2_Split_DebugFileInAnotherDirectory() binaryNotFound.DebugFileType.Should().Be(DebugFileType.FromDwo); binaryNotFound.GetLanguage().Should().Be(DwarfLanguage.Unknown); } + + #region CanLoadBinary + + [Fact] + public void CanLoadBinary_ValidElfFile_ReturnsTrue() + { + string fileName = Path.Combine(TestData, "Dwarf/hello-dwarf4-o2"); + ElfBinary.CanLoadBinary(new Uri(fileName)).Should().BeTrue(); + } + + [Fact] + public void CanLoadBinary_PeFile_ReturnsFalse() + { + string fileName = Path.Combine(TestData, "PE/DeterministicBuild/clangcl.14.pe.c.codeview.pdbpagesize_16384.exe"); + ElfBinary.CanLoadBinary(new Uri(fileName)).Should().BeFalse(); + } + + [Fact] + public void CanLoadBinary_NonExistentFile_ReturnsFalse() + { + string fileName = Path.Combine(TestData, "NonExistent/does_not_exist.bin"); + ElfBinary.CanLoadBinary(new Uri(fileName)).Should().BeFalse(); + } + + [Fact] + public void CanLoadBinary_ZeroByteFile_ReturnsFalse() + { + string fileName = Path.Combine(TestData, "Invalid/ZeroByte/WORKSPACE"); + ElfBinary.CanLoadBinary(new Uri(fileName)).Should().BeFalse(); + } + + #endregion + + #region Error Handling + + [Fact] + public void Constructor_InvalidElfFile_SetsValidFalse() + { + // A zero-byte file cannot be a valid ELF + string fileName = Path.Combine(TestData, "Invalid/ZeroByte/WORKSPACE"); + var binary = new ElfBinary(new Uri(fileName)); + binary.Valid.Should().BeFalse(); + binary.LoadException.Should().NotBeNull(); + } + + [Fact] + public void Constructor_PeFile_SetsValidFalse() + { + string fileName = Path.Combine(TestData, "PE/DeterministicBuild/clangcl.14.pe.c.codeview.pdbpagesize_16384.exe"); + var binary = new ElfBinary(new Uri(fileName)); + binary.Valid.Should().BeFalse(); + binary.LoadException.Should().NotBeNull(); + } + + #endregion + + #region GetSegmentFlags + + [Fact] + public void GetSegmentFlags_GnuStack_ReturnsFlags() + { + // A full binary with debug should have a GNU_STACK segment + string fileName = Path.Combine(TestData, "Dwarf/hello-dwarf4-o2"); + using var binary = new ElfBinary(new Uri(fileName)); + binary.Valid.Should().BeTrue(); + + SegmentFlags? flags = binary.GetSegmentFlags(ElfSegmentType.PT_GNU_STACK); + flags.Should().NotBeNull(); + } + + [Fact] + public void GetSegmentFlags_NonExistentSegment_ReturnsNull() + { + string fileName = Path.Combine(TestData, "Dwarf/hello-dwarf4-o2"); + using var binary = new ElfBinary(new Uri(fileName)); + binary.Valid.Should().BeTrue(); + + // PT_SHLIB is reserved and not used in practice + SegmentFlags? flags = binary.GetSegmentFlags(ElfSegmentType.PT_SHLIB); + flags.Should().BeNull(); + } + + #endregion + + #region GetSymbolTableFiles + + [Fact] + public void GetSymbolTableFiles_BinaryWithSymbols_ReturnsEntries() + { + // The full debug-included binary should have a symbol table with file entries + string fileName = Path.Combine(TestData, "Dwarf/DebugFileType/BinaryDirectory/gcc.objcopy.stripall.addgnudebuglink.full"); + using var binary = new ElfBinary(new Uri(fileName)); + binary.Valid.Should().BeTrue(); + + List> files = binary.GetSymbolTableFiles(); + files.Should().NotBeNull(); + files.Count.Should().BeGreaterThan(0); + } + + [Fact] + public void GetSymbolTableFiles_StrippedBinary_ReturnsEmptyList() + { + // A stripped binary has no symbol table + string fileName = Path.Combine(TestData, "Dwarf/DebugFileType/BinaryDirectory/gcc.objcopy.stripall.addgnudebuglink.nolink"); + using var binary = new ElfBinary(new Uri(fileName)); + binary.Valid.Should().BeTrue(); + + List> files = binary.GetSymbolTableFiles(); + files.Should().NotBeNull(); + files.Should().BeEmpty(); + } + + #endregion + + #region NormalizeAddress + + [Fact] + public void NormalizeAddress_AddressInAllocatableSection_NormalizesCorrectly() + { + string fileName = Path.Combine(TestData, "Dwarf/hello-dwarf4-o2"); + using var binary = new ElfBinary(new Uri(fileName)); + binary.Valid.Should().BeTrue(); + + // The text section address should be valid + ulong textAddr = binary.TextSectionAddress; + textAddr.Should().NotBe(ulong.MaxValue); + + // NormalizeAddress should not throw and should return a value + ulong normalized = binary.NormalizeAddress(binary.ELF.Sections + .First(s => s.Name == ".text").LoadAddress); + normalized.Should().BeGreaterThan(0UL); + } + + [Fact] + public void NormalizeAddress_ZeroAddress_ReturnsOffsetFromCodeSegment() + { + string fileName = Path.Combine(TestData, "Dwarf/hello-dwarf4-o2"); + using var binary = new ElfBinary(new Uri(fileName)); + binary.Valid.Should().BeTrue(); + + // Address 0 is not in any section, so falls through to default path + ulong normalized = binary.NormalizeAddress(0); + // Should subtract CodeSegmentOffset (which wraps around for 0) + normalized.Should().Be(0UL - binary.CodeSegmentOffset); + } + + #endregion + + #region Properties + + [Fact] + public void PublicSymbols_ValidBinary_ContainsSymbols() + { + string fileName = Path.Combine(TestData, "Dwarf/hello-dwarf4-o2"); + using var binary = new ElfBinary(new Uri(fileName)); + binary.Valid.Should().BeTrue(); + binary.PublicSymbols.Should().NotBeNull(); + binary.PublicSymbols.Count.Should().BeGreaterThan(0); + } + + [Fact] + public void Is64bit_64BitBinary_ReturnsTrue() + { + string fileName = Path.Combine(TestData, "Dwarf/hello-dwarf4-o2"); + using var binary = new ElfBinary(new Uri(fileName)); + binary.Valid.Should().BeTrue(); + binary.Is64bit.Should().BeTrue(); + } + + [Fact] + public void TextSectionAddress_ValidBinary_ReturnsNonMaxValue() + { + string fileName = Path.Combine(TestData, "Dwarf/hello-dwarf4-o2"); + using var binary = new ElfBinary(new Uri(fileName)); + binary.Valid.Should().BeTrue(); + binary.TextSectionAddress.Should().NotBe(ulong.MaxValue); + } + + [Fact] + public void DataSectionAddress_ValidBinary_ReturnsNonMaxValue() + { + string fileName = Path.Combine(TestData, "Dwarf/hello-dwarf4-o2"); + using var binary = new ElfBinary(new Uri(fileName)); + binary.Valid.Should().BeTrue(); + binary.DataSectionAddress.Should().NotBe(ulong.MaxValue); + } + + [Fact] + public void EhFrameAddress_ValidBinary_ReturnsNonMaxValue() + { + string fileName = Path.Combine(TestData, "Dwarf/hello-dwarf4-o2"); + using var binary = new ElfBinary(new Uri(fileName)); + binary.Valid.Should().BeTrue(); + binary.EhFrameAddress.Should().NotBe(ulong.MaxValue); + } + + [Fact] + public void CodeSegmentOffset_ValidBinary_IsAccessible() + { + string fileName = Path.Combine(TestData, "Dwarf/hello-dwarf4-o2"); + using var binary = new ElfBinary(new Uri(fileName)); + binary.Valid.Should().BeTrue(); + // CodeSegmentOffset is derived from ProgramHeader segment; value depends on binary layout + // Just verify it's accessible without throwing + binary.CodeSegmentOffset.Should().BeGreaterThanOrEqualTo(0UL); + } + + [Fact] + public void IsDebugOnlyFile_FullBinary_ReturnsFalse() + { + string fileName = Path.Combine(TestData, "Dwarf/hello-dwarf4-o2"); + using var binary = new ElfBinary(new Uri(fileName)); + binary.Valid.Should().BeTrue(); + binary.IsDebugOnlyFile.Should().BeFalse(); + } + + [Fact] + public void IsDebugOnlyFile_DebugOnlyFile_ReturnsTrue() + { + string fileName = Path.Combine(TestData, "Dwarf/DebugFileType/AnotherLocalSymbolDirectory/gcc.objcopy.stripall.addgnudebuglink.dbg"); + using var binary = new ElfBinary(new Uri(fileName)); + binary.Valid.Should().BeTrue(); + binary.IsDebugOnlyFile.Should().BeTrue(); + } + + #endregion + + #region ForceComprehensiveParsing + + [Fact] + public void Constructor_ForceComprehensiveParsing_LoadsAllLazyProperties() + { + string fileName = Path.Combine(TestData, "Dwarf/hello-dwarf4-o2"); + using var binary = new ElfBinary(new Uri(fileName), forceComprehensiveParsing: true); + binary.Valid.Should().BeTrue(); + + // When forceComprehensiveParsing is true, all lazy properties should already be evaluated + binary.CompilationUnits.IsValueCreated.Should().BeTrue(); + binary.LineNumberPrograms.IsValueCreated.Should().BeTrue(); + binary.CommonInformationEntries.IsValueCreated.Should().BeTrue(); + } + + [Fact] + public void Constructor_WithoutForceComprehensiveParsing_LazyPropertiesNotLoaded() + { + string fileName = Path.Combine(TestData, "Dwarf/hello-dwarf4-o2"); + using var binary = new ElfBinary(new Uri(fileName), forceComprehensiveParsing: false); + binary.Valid.Should().BeTrue(); + + // Without forcing, lazy properties should not be evaluated yet + binary.CompilationUnits.IsValueCreated.Should().BeFalse(); + binary.LineNumberPrograms.IsValueCreated.Should().BeFalse(); + binary.CommonInformationEntries.IsValueCreated.Should().BeFalse(); + } + + #endregion } } From c6719c78326591d09a06fad75a9d1f4758eb6510 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20=C5=A0vendov=C3=A1?= Date: Thu, 28 May 2026 14:04:12 +0200 Subject: [PATCH 13/36] Add unit tests for ELF binary analysis in AnalyzeCommandIntegrationTests --- .../AnalyzeCommandIntegrationTests.cs | 276 ++++++++++++++++++ 1 file changed, 276 insertions(+) diff --git a/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs b/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs index 0568e7c9..5a02aaf3 100644 --- a/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs +++ b/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs @@ -298,5 +298,281 @@ private static string GetFunctionalTestDataPath(params string[] relativeParts) return fullPath; } + + /// + /// Resolves a path into the BinaryParsers unit test data directory. + /// + private static string GetBinaryParsersTestDataPath(params string[] relativeParts) + { + string assemblyDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + string testDataRoot = Path.GetFullPath( + Path.Combine(assemblyDir, "..", "..", "..", "..", "src", + "Test.UnitTests.BinaryParsers", "TestData")); + string fullPath = Path.Combine(new[] { testDataRoot }.Concat(relativeParts).ToArray()); + + if (!File.Exists(fullPath) && !Directory.Exists(fullPath)) + { + throw new FileNotFoundException( + $"BinaryParsers test data not found at: {fullPath}"); + } + + return fullPath; + } + + #region ELF Binary Analysis + + [Fact] + public async Task Analyze_ElfBinary_ExitsWithZero() + { + string elfBinary = GetFunctionalTestDataPath( + "BA3006.EnableNonExecutableStack", "Pass", "gcc.helloworld.noexecstack.5.o"); + string sarifOutput = Path.Combine(_tempDir, "elf-output.sarif"); + + BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] + { + "analyze", + elfBinary, + "-o", sarifOutput, + "--kind", "Fail;Pass;NotApplicable", + "--level", "Error;Warning;Note", + }); + + result.ExitCode.Should().Be(0, + $"BinSkim should analyze ELF binaries successfully.\nStdOut: {result.StdOut}\nStdErr: {result.StdErr}"); + } + + [Fact] + public async Task Analyze_ElfBinary_ProducesValidSarif() + { + string elfBinary = GetFunctionalTestDataPath( + "BA3001.EnablePositionIndependentExecutable", "Pass", "gcc.pie_executable"); + string sarifOutput = Path.Combine(_tempDir, "elf-sarif.sarif"); + + BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] + { + "analyze", + elfBinary, + "-o", sarifOutput, + "--kind", "Fail;Pass;NotApplicable", + "--level", "Error;Warning;Note", + }); + + result.ExitCode.Should().Be(0, + $"BinSkim should complete ELF analysis.\nStdErr: {result.StdErr}"); + + SarifLog sarifLog = result.LoadSarifLog(); + sarifLog.Should().NotBeNull(); + sarifLog.Runs.Should().HaveCount(1); + sarifLog.Runs[0].Tool.Driver.Name.Should().Be("BinSkim"); + sarifLog.Runs[0].Results.Should().NotBeEmpty( + "ELF rules should produce results for a valid ELF executable"); + } + + [Fact] + public async Task Analyze_ElfBinary_KnownFail_ReportsElfRule() + { + string failBinary = GetFunctionalTestDataPath( + "BA3006.EnableNonExecutableStack", "Fail", "gcc.helloworld.execstack.5.o"); + string sarifOutput = Path.Combine(_tempDir, "elf-fail.sarif"); + + BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] + { + "analyze", + failBinary, + "-o", sarifOutput, + "--run-only-rules", "BA3006", + "--kind", "Fail", + "--level", "Error;Warning;Note", + }); + + result.ExitCode.Should().Be(0, + $"BinSkim should complete analysis.\nStdErr: {result.StdErr}"); + + SarifLog sarifLog = result.LoadSarifLog(); + sarifLog.Should().NotBeNull(); + sarifLog.Runs[0].Results.Should().Contain( + r => r.RuleId == "BA3006" && r.Level == FailureLevel.Error, + "BA3006 should fire an error for an ELF binary with executable stack"); + } + + [Fact] + public async Task Analyze_ElfBinary_KnownPass_ReportsPass() + { + string passBinary = GetFunctionalTestDataPath( + "BA3001.EnablePositionIndependentExecutable", "Pass", "gcc.pie_executable"); + string sarifOutput = Path.Combine(_tempDir, "elf-pass.sarif"); + + BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] + { + "analyze", + passBinary, + "-o", sarifOutput, + "--run-only-rules", "BA3001", + "--kind", "Fail;Pass", + "--level", "Error;Warning;Note", + }); + + result.ExitCode.Should().Be(0, + $"BinSkim should complete analysis.\nStdErr: {result.StdErr}"); + + SarifLog sarifLog = result.LoadSarifLog(); + sarifLog.Should().NotBeNull(); + sarifLog.Runs[0].Results.Should().Contain( + r => r.RuleId == "BA3001" && r.Level == FailureLevel.None, + "BA3001 should pass for a PIE executable"); + } + + #endregion + + #region Multi-target and Glob + + [Fact] + public async Task Analyze_MultipleTargets_ScansAll() + { + string target1 = BinSkimRunner.GetBinSkimDllPath(); + string target2 = GetFunctionalTestDataPath( + "BA2016.MarkImageAsNXCompatible", "Fail", "ManagedFail.dll"); + string sarifOutput = Path.Combine(_tempDir, "multi.sarif"); + + BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] + { + "analyze", + target1, + target2, + "-o", sarifOutput, + "--kind", "Fail;Pass;NotApplicable", + "--level", "Error;Warning;Note", + }); + + result.ExitCode.Should().Be(0, + $"BinSkim should handle multiple targets.\nStdErr: {result.StdErr}"); + + SarifLog sarifLog = result.LoadSarifLog(); + sarifLog.Should().NotBeNull(); + + // Results should reference both target files + var targetUris = sarifLog.Runs[0].Results + .Select(r => r.Locations?.FirstOrDefault()?.PhysicalLocation?.ArtifactLocation?.Uri?.ToString() ?? "") + .Distinct() + .ToList(); + + targetUris.Should().HaveCountGreaterThan(1, + "results should come from multiple target binaries"); + } + + [Fact] + public async Task Analyze_Recurse_FindsNestedBinaries() + { + // Set up a temp directory tree with binaries + string subDir = Path.Combine(_tempDir, "nested"); + Directory.CreateDirectory(subDir); + + string source = BinSkimRunner.GetBinSkimDllPath(); + string copy1 = Path.Combine(_tempDir, "top.dll"); + string copy2 = Path.Combine(subDir, "nested.dll"); + File.Copy(source, copy1); + File.Copy(source, copy2); + + string sarifOutput = Path.Combine(_tempDir, "recurse.sarif"); + + BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] + { + "analyze", + Path.Combine(_tempDir, "*.dll"), + "-o", sarifOutput, + "--recurse", "True", + "--kind", "Fail;Pass;NotApplicable", + "--level", "Error;Warning;Note", + }); + + result.ExitCode.Should().Be(0, + $"BinSkim should recurse directories.\nStdErr: {result.StdErr}"); + + SarifLog sarifLog = result.LoadSarifLog(); + sarifLog.Should().NotBeNull(); + + var analyzedFiles = sarifLog.Runs[0].Results + .Select(r => r.Locations?.FirstOrDefault()?.PhysicalLocation?.ArtifactLocation?.Uri?.ToString() ?? "") + .Distinct() + .ToList(); + + analyzedFiles.Should().HaveCountGreaterThan(1, + "recurse should find binaries in subdirectories"); + } + + #endregion + + #region Rich Return Code + + [Fact] + public async Task Analyze_RichReturnCode_InvalidTarget_ReturnsRuntimeConditions() + { + string nonExistentTarget = Path.Combine(_tempDir, "does_not_exist.dll"); + + BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] + { + "analyze", + nonExistentTarget, + "--rich-return-code", + }); + + // With rich return code, the exit code encodes RuntimeConditions flags + result.ExitCode.Should().NotBe(0, + "rich return code should return non-zero for invalid targets"); + result.ExitCode.Should().NotBe(1, + "rich return code should return a RuntimeConditions bitmask, not simple 1"); + } + + #endregion + + #region Local Symbol Directories + + [Fact] + public async Task Analyze_LocalSymbolDirectories_AcceptsOption() + { + string elfBinary = GetBinaryParsersTestDataPath("Dwarf", "hello-dwarf4-o2"); + string sarifOutput = Path.Combine(_tempDir, "symdir.sarif"); + + BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] + { + "analyze", + elfBinary, + "-o", sarifOutput, + "--local-symbol-directories", _tempDir, + "--kind", "Fail;Pass;NotApplicable", + "--level", "Error;Warning;Note", + }); + + result.ExitCode.Should().Be(0, + $"BinSkim should accept --local-symbol-directories.\nStdErr: {result.StdErr}"); + } + + #endregion + + #region Trace Output + + [Fact] + public async Task Analyze_TraceTargetsScanned_ProducesTraceOutput() + { + string targetBinary = BinSkimRunner.GetBinSkimDllPath(); + string sarifOutput = Path.Combine(_tempDir, "trace.sarif"); + + BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] + { + "analyze", + targetBinary, + "-o", sarifOutput, + "--trace", "TargetsScanned;ResultsSummary", + }); + + result.ExitCode.Should().Be(0, + $"BinSkim should accept trace flags.\nStdErr: {result.StdErr}"); + + string combined = result.StdOut + result.StdErr; + combined.Should().NotBeNullOrWhiteSpace( + "trace output should produce console output"); + } + + #endregion } } From 93c684244bf6c6b636137b9b024f85ac76649f24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20=C5=A0vendov=C3=A1?= Date: Thu, 28 May 2026 14:18:52 +0200 Subject: [PATCH 14/36] Add Pester integration tests for BinSkim CLI functionality --- .../BinSkim.Integration.Tests.ps1 | 395 ++++++++++++++++++ 1 file changed, 395 insertions(+) create mode 100644 src/Test.IntegrationTests.BinSkim.Driver/BinSkim.Integration.Tests.ps1 diff --git a/src/Test.IntegrationTests.BinSkim.Driver/BinSkim.Integration.Tests.ps1 b/src/Test.IntegrationTests.BinSkim.Driver/BinSkim.Integration.Tests.ps1 new file mode 100644 index 00000000..3cd9563c --- /dev/null +++ b/src/Test.IntegrationTests.BinSkim.Driver/BinSkim.Integration.Tests.ps1 @@ -0,0 +1,395 @@ +# Copyright (c) Microsoft. All rights reserved. +# Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#Requires -Modules Pester + +<# +.SYNOPSIS + Pester integration tests for BinSkim CLI. + Validates end-to-end behavior by invoking BinSkim as an external process. + +.DESCRIPTION + These tests mirror the C# integration tests in AnalyzeCommandIntegrationTests.cs + but are written in PowerShell using the Pester framework for use in pipeline scenarios. + +.EXAMPLE + Invoke-Pester -Path ./BinSkim.Integration.Tests.ps1 +#> + +BeforeAll { + $RepoRoot = (Resolve-Path "$PSScriptRoot/../..").Path + $BinSkimDll = Join-Path $RepoRoot "bld/bin/BinSkim.Driver/release/BinSkim.dll" + + if (-not (Test-Path $BinSkimDll)) { + # Try debug build + $BinSkimDll = Join-Path $RepoRoot "bld/bin/BinSkim.Driver/debug/BinSkim.dll" + } + + if (-not (Test-Path $BinSkimDll)) { + throw "BinSkim.dll not found. Build the BinSkim.Driver project first." + } + + $FunctionalTestData = Join-Path $RepoRoot "src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData" + $BinaryParsersTestData = Join-Path $RepoRoot "src/Test.UnitTests.BinaryParsers/TestData" + + function Invoke-BinSkim { + param( + [string[]]$Arguments + ) + $process = Start-Process -FilePath "dotnet" -ArgumentList (@($BinSkimDll) + $Arguments) ` + -NoNewWindow -Wait -PassThru -RedirectStandardOutput "$env:TEMP\binskim_stdout.txt" ` + -RedirectStandardError "$env:TEMP\binskim_stderr.txt" + + return @{ + ExitCode = $process.ExitCode + StdOut = (Get-Content "$env:TEMP\binskim_stdout.txt" -Raw -ErrorAction SilentlyContinue) ?? "" + StdErr = (Get-Content "$env:TEMP\binskim_stderr.txt" -Raw -ErrorAction SilentlyContinue) ?? "" + } + } + + function Get-SarifLog { + param([string]$Path) + if (Test-Path $Path) { + return Get-Content $Path -Raw | ConvertFrom-Json + } + return $null + } +} + +Describe "BinSkim CLI - Self Scan" { + BeforeAll { + $script:TempDir = Join-Path ([System.IO.Path]::GetTempPath()) "BinSkimPester_$([guid]::NewGuid().ToString('N'))" + New-Item -ItemType Directory -Path $script:TempDir -Force | Out-Null + } + + AfterAll { + Remove-Item $script:TempDir -Recurse -Force -ErrorAction SilentlyContinue + } + + It "Analyze self-scan exits with zero" { + $sarifOutput = Join-Path $script:TempDir "selfscan.sarif" + $result = Invoke-BinSkim -Arguments @("analyze", $BinSkimDll, "-o", $sarifOutput) + $result.ExitCode | Should -Be 0 + } + + It "Analyze self-scan produces valid SARIF" { + $sarifOutput = Join-Path $script:TempDir "selfscan-sarif.sarif" + $result = Invoke-BinSkim -Arguments @("analyze", $BinSkimDll, "-o", $sarifOutput) + $result.ExitCode | Should -Be 0 + + $sarif = Get-SarifLog -Path $sarifOutput + $sarif | Should -Not -BeNullOrEmpty + $sarif.runs | Should -HaveCount 1 + $sarif.runs[0].tool.driver.name | Should -Be "BinSkim" + } +} + +Describe "BinSkim CLI - ELF Binary Analysis" { + BeforeAll { + $script:TempDir = Join-Path ([System.IO.Path]::GetTempPath()) "BinSkimPesterElf_$([guid]::NewGuid().ToString('N'))" + New-Item -ItemType Directory -Path $script:TempDir -Force | Out-Null + } + + AfterAll { + Remove-Item $script:TempDir -Recurse -Force -ErrorAction SilentlyContinue + } + + It "Analyze ELF binary exits with zero" { + $elfBinary = Join-Path $FunctionalTestData "BA3006.EnableNonExecutableStack/Pass/gcc.helloworld.noexecstack.5.o" + $sarifOutput = Join-Path $script:TempDir "elf-exit.sarif" + + $result = Invoke-BinSkim -Arguments @( + "analyze", $elfBinary, "-o", $sarifOutput, + "--kind", "Fail;Pass;NotApplicable", "--level", "Error;Warning;Note" + ) + $result.ExitCode | Should -Be 0 + } + + It "Analyze ELF binary produces valid SARIF with results" { + $elfBinary = Join-Path $FunctionalTestData "BA3001.EnablePositionIndependentExecutable/Pass/gcc.pie_executable" + $sarifOutput = Join-Path $script:TempDir "elf-sarif.sarif" + + $result = Invoke-BinSkim -Arguments @( + "analyze", $elfBinary, "-o", $sarifOutput, + "--kind", "Fail;Pass;NotApplicable", "--level", "Error;Warning;Note" + ) + $result.ExitCode | Should -Be 0 + + $sarif = Get-SarifLog -Path $sarifOutput + $sarif | Should -Not -BeNullOrEmpty + $sarif.runs[0].tool.driver.name | Should -Be "BinSkim" + $sarif.runs[0].results.Count | Should -BeGreaterThan 0 + } + + It "Analyze ELF known-fail binary reports BA3006 error" { + $failBinary = Join-Path $FunctionalTestData "BA3006.EnableNonExecutableStack/Fail/gcc.helloworld.execstack.5.o" + $sarifOutput = Join-Path $script:TempDir "elf-fail.sarif" + + $result = Invoke-BinSkim -Arguments @( + "analyze", $failBinary, "-o", $sarifOutput, + "--run-only-rules", "BA3006", "--kind", "Fail", "--level", "Error;Warning;Note" + ) + $result.ExitCode | Should -Be 0 + + $sarif = Get-SarifLog -Path $sarifOutput + $sarif | Should -Not -BeNullOrEmpty + $errorResults = $sarif.runs[0].results | Where-Object { $_.ruleId -eq "BA3006" -and $_.level -eq "error" } + $errorResults | Should -Not -BeNullOrEmpty + } + + It "Analyze ELF known-pass binary reports BA3001 pass" { + $passBinary = Join-Path $FunctionalTestData "BA3001.EnablePositionIndependentExecutable/Pass/gcc.pie_executable" + $sarifOutput = Join-Path $script:TempDir "elf-pass.sarif" + + $result = Invoke-BinSkim -Arguments @( + "analyze", $passBinary, "-o", $sarifOutput, + "--run-only-rules", "BA3001", "--kind", "Fail;Pass", "--level", "Error;Warning;Note" + ) + $result.ExitCode | Should -Be 0 + + $sarif = Get-SarifLog -Path $sarifOutput + $sarif | Should -Not -BeNullOrEmpty + $passResults = $sarif.runs[0].results | Where-Object { $_.ruleId -eq "BA3001" -and $_.level -eq "none" } + $passResults | Should -Not -BeNullOrEmpty + } +} + +Describe "BinSkim CLI - Multi-target and Glob" { + BeforeAll { + $script:TempDir = Join-Path ([System.IO.Path]::GetTempPath()) "BinSkimPesterMulti_$([guid]::NewGuid().ToString('N'))" + New-Item -ItemType Directory -Path $script:TempDir -Force | Out-Null + } + + AfterAll { + Remove-Item $script:TempDir -Recurse -Force -ErrorAction SilentlyContinue + } + + It "Analyze multiple targets scans all" { + $target1 = $BinSkimDll + $failBinary = Join-Path $FunctionalTestData "BA2016.MarkImageAsNXCompatible/Fail/ManagedFail.dll" + $sarifOutput = Join-Path $script:TempDir "multi.sarif" + + $result = Invoke-BinSkim -Arguments @( + "analyze", $target1, $failBinary, "-o", $sarifOutput, + "--kind", "Fail;Pass;NotApplicable", "--level", "Error;Warning;Note" + ) + $result.ExitCode | Should -Be 0 + + $sarif = Get-SarifLog -Path $sarifOutput + $sarif | Should -Not -BeNullOrEmpty + $uris = $sarif.runs[0].results | ForEach-Object { + $_.locations[0].physicalLocation.artifactLocation.uri + } | Sort-Object -Unique + $uris.Count | Should -BeGreaterThan 1 + } + + It "Analyze with recurse finds nested binaries" { + $subDir = Join-Path $script:TempDir "nested" + New-Item -ItemType Directory -Path $subDir -Force | Out-Null + + Copy-Item $BinSkimDll (Join-Path $script:TempDir "top.dll") + Copy-Item $BinSkimDll (Join-Path $subDir "nested.dll") + + $sarifOutput = Join-Path $script:TempDir "recurse.sarif" + $globPattern = Join-Path $script:TempDir "*.dll" + + $result = Invoke-BinSkim -Arguments @( + "analyze", $globPattern, "-o", $sarifOutput, + "--recurse", "True", "--kind", "Fail;Pass;NotApplicable", "--level", "Error;Warning;Note" + ) + $result.ExitCode | Should -Be 0 + + $sarif = Get-SarifLog -Path $sarifOutput + $sarif | Should -Not -BeNullOrEmpty + $uris = $sarif.runs[0].results | ForEach-Object { + $_.locations[0].physicalLocation.artifactLocation.uri + } | Sort-Object -Unique + $uris.Count | Should -BeGreaterThan 1 + } +} + +Describe "BinSkim CLI - Rich Return Code" { + BeforeAll { + $script:TempDir = Join-Path ([System.IO.Path]::GetTempPath()) "BinSkimPesterRRC_$([guid]::NewGuid().ToString('N'))" + New-Item -ItemType Directory -Path $script:TempDir -Force | Out-Null + } + + AfterAll { + Remove-Item $script:TempDir -Recurse -Force -ErrorAction SilentlyContinue + } + + It "Rich return code returns non-zero RuntimeConditions for invalid target" { + $nonExistent = Join-Path $script:TempDir "does_not_exist.dll" + + $result = Invoke-BinSkim -Arguments @("analyze", $nonExistent, "--rich-return-code") + + $result.ExitCode | Should -Not -Be 0 + $result.ExitCode | Should -Not -Be 1 + } +} + +Describe "BinSkim CLI - Local Symbol Directories" { + BeforeAll { + $script:TempDir = Join-Path ([System.IO.Path]::GetTempPath()) "BinSkimPesterSym_$([guid]::NewGuid().ToString('N'))" + New-Item -ItemType Directory -Path $script:TempDir -Force | Out-Null + } + + AfterAll { + Remove-Item $script:TempDir -Recurse -Force -ErrorAction SilentlyContinue + } + + It "Accepts --local-symbol-directories option" { + $elfBinary = Join-Path $BinaryParsersTestData "Dwarf/hello-dwarf4-o2" + $sarifOutput = Join-Path $script:TempDir "symdir.sarif" + + $result = Invoke-BinSkim -Arguments @( + "analyze", $elfBinary, "-o", $sarifOutput, + "--local-symbol-directories", $script:TempDir, + "--kind", "Fail;Pass;NotApplicable", "--level", "Error;Warning;Note" + ) + $result.ExitCode | Should -Be 0 + } +} + +Describe "BinSkim CLI - Trace Output" { + BeforeAll { + $script:TempDir = Join-Path ([System.IO.Path]::GetTempPath()) "BinSkimPesterTrace_$([guid]::NewGuid().ToString('N'))" + New-Item -ItemType Directory -Path $script:TempDir -Force | Out-Null + } + + AfterAll { + Remove-Item $script:TempDir -Recurse -Force -ErrorAction SilentlyContinue + } + + It "Trace flags produce output" { + $sarifOutput = Join-Path $script:TempDir "trace.sarif" + + $result = Invoke-BinSkim -Arguments @( + "analyze", $BinSkimDll, "-o", $sarifOutput, + "--trace", "TargetsScanned;ResultsSummary" + ) + $result.ExitCode | Should -Be 0 + + $combined = $result.StdOut + $result.StdErr + $combined | Should -Not -BeNullOrEmpty + } +} + +Describe "BinSkim CLI - Error Handling" { + It "Non-existent target exits with non-zero" { + $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) "BinSkimPesterErr_$([guid]::NewGuid().ToString('N'))" + New-Item -ItemType Directory -Path $tempDir -Force | Out-Null + + try { + $nonExistent = Join-Path $tempDir "does_not_exist.dll" + $sarifOutput = Join-Path $tempDir "output.sarif" + + $result = Invoke-BinSkim -Arguments @("analyze", $nonExistent, "-o", $sarifOutput) + $result.ExitCode | Should -Not -Be 0 + } + finally { + Remove-Item $tempDir -Recurse -Force -ErrorAction SilentlyContinue + } + } + + It "Invalid argument exits with non-zero" { + $result = Invoke-BinSkim -Arguments @("analyze", "--bogus-flag") + $result.ExitCode | Should -Not -Be 0 + } + + It "Invalid verb exits with non-zero" { + $result = Invoke-BinSkim -Arguments @("not-a-real-verb") + $result.ExitCode | Should -Not -Be 0 + } +} + +Describe "BinSkim CLI - Help and Version" { + It "Help flag exits cleanly" { + $result = Invoke-BinSkim -Arguments @("help") + $result.ExitCode | Should -Be 0 + ($result.StdOut + $result.StdErr) | Should -Not -BeNullOrEmpty + } + + It "--version exits cleanly" { + $result = Invoke-BinSkim -Arguments @("--version") + $result.ExitCode | Should -Be 0 + ($result.StdOut + $result.StdErr) | Should -Not -BeNullOrEmpty + } +} + +Describe "BinSkim CLI - Known Fail Binary" { + BeforeAll { + $script:TempDir = Join-Path ([System.IO.Path]::GetTempPath()) "BinSkimPesterFail_$([guid]::NewGuid().ToString('N'))" + New-Item -ItemType Directory -Path $script:TempDir -Force | Out-Null + } + + AfterAll { + Remove-Item $script:TempDir -Recurse -Force -ErrorAction SilentlyContinue + } + + It "Known PE fail binary produces error results" { + $failBinary = Join-Path $FunctionalTestData "BA2016.MarkImageAsNXCompatible/Fail/ManagedFail.dll" + $sarifOutput = Join-Path $script:TempDir "pe-fail.sarif" + + $result = Invoke-BinSkim -Arguments @( + "analyze", $failBinary, "-o", $sarifOutput, + "--run-only-rules", "BA2016", "--kind", "Fail", "--level", "Error;Warning;Note" + ) + $result.ExitCode | Should -Be 0 + + $sarif = Get-SarifLog -Path $sarifOutput + $sarif | Should -Not -BeNullOrEmpty + $errorResults = $sarif.runs[0].results | Where-Object { $_.ruleId -eq "BA2016" -and $_.level -eq "error" } + $errorResults | Should -Not -BeNullOrEmpty + } +} + +Describe "BinSkim CLI - Dump Command" { + It "Dump self-scan produces metadata output" { + $result = Invoke-BinSkim -Arguments @("dump", $BinSkimDll) + $result.ExitCode | Should -Be 0 + $result.StdOut | Should -Match "BinSkim" + } + + It "Dump verbose produces more detailed output" { + $normalResult = Invoke-BinSkim -Arguments @("dump", $BinSkimDll) + $verboseResult = Invoke-BinSkim -Arguments @("dump", $BinSkimDll, "--verbose") + + $normalResult.ExitCode | Should -Be 0 + $verboseResult.ExitCode | Should -Be 0 + $verboseResult.StdOut.Length | Should -BeGreaterOrEqual $normalResult.StdOut.Length + } +} + +Describe "BinSkim CLI - Export Commands" { + BeforeAll { + $script:TempDir = Join-Path ([System.IO.Path]::GetTempPath()) "BinSkimPesterExport_$([guid]::NewGuid().ToString('N'))" + New-Item -ItemType Directory -Path $script:TempDir -Force | Out-Null + } + + AfterAll { + Remove-Item $script:TempDir -Recurse -Force -ErrorAction SilentlyContinue + } + + It "Export-rules produces SARIF with rule metadata" { + $outputPath = Join-Path $script:TempDir "rules.sarif" + + $result = Invoke-BinSkim -Arguments @("export-rules", $outputPath) + $result.ExitCode | Should -Be 0 + + Test-Path $outputPath | Should -BeTrue + $content = Get-Content $outputPath -Raw + $content | Should -Match "BA2016" + } + + It "Export-config produces JSON config" { + $outputPath = Join-Path $script:TempDir "config.json" + + $result = Invoke-BinSkim -Arguments @("export-config", $outputPath) + $result.ExitCode | Should -Be 0 + + Test-Path $outputPath | Should -BeTrue + $content = Get-Content $outputPath -Raw + $content | Should -Not -BeNullOrEmpty + } +} From 16fe7461fbe4e38b8a896d0bf673ca0db5a39e37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20=C5=A0vendov=C3=A1?= Date: Thu, 4 Jun 2026 11:07:00 +0200 Subject: [PATCH 15/36] Add PDB file for BA2004.EnableSecureSourceCodeHashing functional test failure case --- .../Managed_net48_full_exe_sha1.exe | Bin .../Managed_net48_full_exe_sha1.pdb | Bin .../{ => Error}/Managed_net48_full_sha1.dll | Bin .../{ => Error}/Managed_net48_full_sha1.pdb | Bin .../Managed_net48_portable_exe_sha1.exe | Bin .../Managed_net48_portable_exe_sha1.pdb | Bin .../Managed_net48_portable_sha1.dll | Bin .../Managed_net48_portable_sha1.pdb | Bin ...anaged_netcoreapp3.1_portable_exe_sha1.dll | Bin ...anaged_netcoreapp3.1_portable_exe_sha1.pdb | Bin .../Managed_netstandard2.0_portable_sha1.dll | Bin .../Managed_netstandard2.0_portable_sha1.pdb | Bin .../{ => Warning}/Native_x64_VS2019_MD5.exe | Bin .../{ => Warning}/Native_x64_VS2019_MD5.pdb | Bin .../Native_x64_VS2022_WithPCH_sha1.exe | Bin .../Native_x64_VS2022_WithPCH_sha1.pdb | Bin .../RuleTests.cs | 420 ++++++++++-------- 17 files changed, 229 insertions(+), 191 deletions(-) rename src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/{ => Error}/Managed_net48_full_exe_sha1.exe (100%) rename src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/{ => Error}/Managed_net48_full_exe_sha1.pdb (100%) rename src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/{ => Error}/Managed_net48_full_sha1.dll (100%) rename src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/{ => Error}/Managed_net48_full_sha1.pdb (100%) rename src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/{ => Error}/Managed_net48_portable_exe_sha1.exe (100%) rename src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/{ => Error}/Managed_net48_portable_exe_sha1.pdb (100%) rename src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/{ => Error}/Managed_net48_portable_sha1.dll (100%) rename src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/{ => Error}/Managed_net48_portable_sha1.pdb (100%) rename src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/{ => Error}/Managed_netcoreapp3.1_portable_exe_sha1.dll (100%) rename src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/{ => Error}/Managed_netcoreapp3.1_portable_exe_sha1.pdb (100%) rename src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/{ => Error}/Managed_netstandard2.0_portable_sha1.dll (100%) rename src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/{ => Error}/Managed_netstandard2.0_portable_sha1.pdb (100%) rename src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/{ => Warning}/Native_x64_VS2019_MD5.exe (100%) rename src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/{ => Warning}/Native_x64_VS2019_MD5.pdb (100%) rename src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/{ => Warning}/Native_x64_VS2022_WithPCH_sha1.exe (100%) rename src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/{ => Warning}/Native_x64_VS2022_WithPCH_sha1.pdb (100%) diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_net48_full_exe_sha1.exe b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_net48_full_exe_sha1.exe similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_net48_full_exe_sha1.exe rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_net48_full_exe_sha1.exe diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_net48_full_exe_sha1.pdb b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_net48_full_exe_sha1.pdb similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_net48_full_exe_sha1.pdb rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_net48_full_exe_sha1.pdb diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_net48_full_sha1.dll b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_net48_full_sha1.dll similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_net48_full_sha1.dll rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_net48_full_sha1.dll diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_net48_full_sha1.pdb b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_net48_full_sha1.pdb similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_net48_full_sha1.pdb rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_net48_full_sha1.pdb diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_net48_portable_exe_sha1.exe b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_net48_portable_exe_sha1.exe similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_net48_portable_exe_sha1.exe rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_net48_portable_exe_sha1.exe diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_net48_portable_exe_sha1.pdb b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_net48_portable_exe_sha1.pdb similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_net48_portable_exe_sha1.pdb rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_net48_portable_exe_sha1.pdb diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_net48_portable_sha1.dll b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_net48_portable_sha1.dll similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_net48_portable_sha1.dll rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_net48_portable_sha1.dll diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_net48_portable_sha1.pdb b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_net48_portable_sha1.pdb similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_net48_portable_sha1.pdb rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_net48_portable_sha1.pdb diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_netcoreapp3.1_portable_exe_sha1.dll b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_netcoreapp3.1_portable_exe_sha1.dll similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_netcoreapp3.1_portable_exe_sha1.dll rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_netcoreapp3.1_portable_exe_sha1.dll diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_netcoreapp3.1_portable_exe_sha1.pdb b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_netcoreapp3.1_portable_exe_sha1.pdb similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_netcoreapp3.1_portable_exe_sha1.pdb rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_netcoreapp3.1_portable_exe_sha1.pdb diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_netstandard2.0_portable_sha1.dll b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_netstandard2.0_portable_sha1.dll similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_netstandard2.0_portable_sha1.dll rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_netstandard2.0_portable_sha1.dll diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_netstandard2.0_portable_sha1.pdb b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_netstandard2.0_portable_sha1.pdb similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Managed_netstandard2.0_portable_sha1.pdb rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_netstandard2.0_portable_sha1.pdb diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Native_x64_VS2019_MD5.exe b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Native_x64_VS2019_MD5.exe similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Native_x64_VS2019_MD5.exe rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Native_x64_VS2019_MD5.exe diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Native_x64_VS2019_MD5.pdb b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Native_x64_VS2019_MD5.pdb similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Native_x64_VS2019_MD5.pdb rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Native_x64_VS2019_MD5.pdb diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Native_x64_VS2022_WithPCH_sha1.exe b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Native_x64_VS2022_WithPCH_sha1.exe similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Native_x64_VS2022_WithPCH_sha1.exe rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Native_x64_VS2022_WithPCH_sha1.exe diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Native_x64_VS2022_WithPCH_sha1.pdb b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Native_x64_VS2022_WithPCH_sha1.pdb similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Native_x64_VS2022_WithPCH_sha1.pdb rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Native_x64_VS2022_WithPCH_sha1.pdb diff --git a/src/Test.FunctionalTests.BinSkim.Rules/RuleTests.cs b/src/Test.FunctionalTests.BinSkim.Rules/RuleTests.cs index 50e99a92..88b71856 100644 --- a/src/Test.FunctionalTests.BinSkim.Rules/RuleTests.cs +++ b/src/Test.FunctionalTests.BinSkim.Rules/RuleTests.cs @@ -26,68 +26,91 @@ public RuleTests(ITestOutputHelper output) this.testOutputHelper = output; } + private enum ExpectedOutcome + { + Pass, + Fail + } + private void VerifyPass( BinarySkimmer skimmer, IEnumerable additionalTestFiles = null, - bool useDefaultPolicy = false, - bool bypassExtensionValidation = false, - bool ignoreNoteTargets = false) + bool useDefaultPolicy = false) { - this.Verify(skimmer, additionalTestFiles, useDefaultPolicy, expectToPass: true, bypassExtensionValidation: bypassExtensionValidation, ignoreNoteTargets: ignoreNoteTargets); + this.Verify( + skimmer, + ExpectedOutcome.Pass, + expectedLevel: FailureLevel.None, + additionalTestFiles: additionalTestFiles, + useDefaultPolicy: useDefaultPolicy); } private void VerifyFail( BinarySkimmer skimmer, IEnumerable additionalTestFiles = null, - bool useDefaultPolicy = false, - bool bypassExtensionValidation = false, - bool ignoreNoteTargets = false) + bool useDefaultPolicy = false) { - this.Verify(skimmer, additionalTestFiles, useDefaultPolicy, expectToPass: false, bypassExtensionValidation: bypassExtensionValidation, ignoreNoteTargets: ignoreNoteTargets); + this.Verify( + skimmer, + ExpectedOutcome.Fail, + expectedLevel: FailureLevel.None, + additionalTestFiles: additionalTestFiles, + useDefaultPolicy: useDefaultPolicy); } + private static readonly HashSet ExcludedTestFileExtensions = new HashSet(StringComparer.OrdinalIgnoreCase) { ".pdb" }; + private void Verify( BinarySkimmer skimmer, - IEnumerable additionalTestFiles, - bool useDefaultPolicy, - bool expectToPass, - bool bypassExtensionValidation = false, - bool ignoreNoteTargets = false) + ExpectedOutcome expectedOutcome, + FailureLevel expectedLevel = FailureLevel.None, + ResultKind expectedKind = ResultKind.None, + IEnumerable additionalTestFiles = null, + bool useDefaultPolicy = false, + string outcomeSubdirectory = null) { - var targets = new List(); string ruleName = skimmer.GetType().Name; - string testFilesDirectory = GetTestDirectoryFor(ruleName); - testFilesDirectory = Path.Combine(Environment.CurrentDirectory, "FunctionalTestData", testFilesDirectory); - testFilesDirectory = Path.Combine(testFilesDirectory, expectToPass ? "Pass" : "Fail"); + string outcomeDir = expectedOutcome == ExpectedOutcome.Pass ? "Pass" : "Fail"; + string testFilesDirectory = Path.Combine( + Environment.CurrentDirectory, + "FunctionalTestData", + GetTestDirectoryFor(ruleName), + outcomeSubdirectory != null ? Path.Combine(outcomeDir, outcomeSubdirectory) : outcomeDir); + + Assert.True(Directory.Exists(testFilesDirectory), + $"Test directory '{testFilesDirectory}' should exist."); - Assert.True(Directory.Exists(testFilesDirectory), $"Test directory '{testFilesDirectory}' should exist."); + string[] allFiles = Directory.GetFiles(testFilesDirectory, "*", SearchOption.AllDirectories); + var targets = new List(); - foreach (string target in Directory.GetFiles(testFilesDirectory, "*", SearchOption.AllDirectories)) + foreach (string target in allFiles) { - if (bypassExtensionValidation || MultithreadedAnalyzeCommand.ValidAnalysisFileExtensions.Contains(Path.GetExtension(target))) + if (!ExcludedTestFileExtensions.Contains(Path.GetExtension(target))) { targets.Add(target); } } - if (additionalTestFiles != null) + int excludedCount = allFiles.Length - targets.Count; + Assert.True(targets.Count > 0, + $"All {allFiles.Length} file(s) in '{testFilesDirectory}' were excluded by extension filter " + + $"(excluded: {string.Join(", ", ExcludedTestFileExtensions)})."); + + if (excludedCount > 0) { - foreach (string additionalTestFile in additionalTestFiles) - { - targets.Add(additionalTestFile); - } + this.testOutputHelper.WriteLine( + $"[Info] {excludedCount} file(s) excluded by extension filter (e.g. .pdb). " + + $"Analyzing {targets.Count} of {allFiles.Length} files."); } - var context = new BinaryAnalyzerContext(); - var logger = new TestMessageLogger(); - context.Logger = logger; - PropertiesDictionary policy = null; - - if (useDefaultPolicy) + if (additionalTestFiles != null) { - policy = new PropertiesDictionary(); + targets.AddRange(additionalTestFiles); } - context.Policy = policy; + + var logger = new TestMessageLogger(); + PropertiesDictionary policy = useDefaultPolicy ? new PropertiesDictionary() : null; + var context = new BinaryAnalyzerContext { Logger = logger, Policy = policy }; skimmer.Initialize(context); @@ -107,75 +130,70 @@ private void Verify( skimmer.Analyze(context); } - var failTargets = new HashSet(logger.ErrorTargets.Union(logger.WarningTargets)); - var passTargets = new HashSet(logger.PassTargets); - - if (logger.NoteTargets.Count > 0) - { - if (ignoreNoteTargets) - { - // If a same target has both warning/error and note result. - passTargets.UnionWith(logger.NoteTargets.Except(failTargets)); - } - else - { - failTargets.UnionWith(logger.NoteTargets); - } - } - - HashSet expected = expectToPass ? passTargets : failTargets; - HashSet other = expectToPass ? failTargets : passTargets; - HashSet configErrors = logger.ConfigurationErrorTargets; - - string expectedText = expectToPass ? "success" : "failure"; - string actualText = expectToPass ? "failed" : "succeeded"; var sb = new StringBuilder(); foreach (string target in targets) { - if (expected.Contains(target)) - { - expected.Remove(target); - continue; - } - bool missingEntirely = !other.Contains(target); - - if (missingEntirely && - !expectToPass && - target.Contains("Pdb") && - configErrors.Contains(target)) - { - missingEntirely = false; - configErrors.Remove(target); - continue; - } + string fileName = Path.GetFileName(target); - if (missingEntirely) - { - // Generates message such as the following: - // "Expected 'BA2025:EnableShadowStack' success but saw no result at all for file: Native_x64_CETShadowStack_Disabled.exe" - sb.AppendLine( - string.Format( - "Expected '{0}:{1}' {2} but saw no result at all for file: {3}", - skimmer.Id, - ruleName, - expectedText, - Path.GetFileName(target))); - } - else + switch (expectedOutcome) { - other.Remove(target); + case ExpectedOutcome.Pass: + { + if (!logger.PassTargets.Contains(target)) + { + string actual = GetActualOutcome(logger, target); + sb.AppendLine( + $"Expected '{skimmer.Id}:{ruleName}' Pass (Kind={expectedKind}) " + + $"for '{fileName}' but got: {actual}"); + } + + break; + } - // Generates message such as the following: - // "Expected 'BA2025:EnableShadowStack' success but check failed for: Native_x64_CETShadowStack_Disabled.exe" - sb.AppendLine( - string.Format( - "Expected '{0}:{1}' {2} but check {3} for: {4}", - skimmer.Id, - ruleName, - expectedText, - actualText, - Path.GetFileName(target))); + case ExpectedOutcome.Fail: + { + bool found; + + if (expectedLevel == FailureLevel.None) + { + // Accept any failure level (backwards compat). + found = logger.ErrorTargets.Contains(target) || + logger.WarningTargets.Contains(target) || + logger.NoteTargets.Contains(target); + } + else + { + // Check specific failure level. + found = expectedLevel switch + { + FailureLevel.Error => logger.ErrorTargets.Contains(target), + FailureLevel.Warning => logger.WarningTargets.Contains(target), + FailureLevel.Note => logger.NoteTargets.Contains(target), + _ => false + }; + } + + if (!found) + { + // Allow config errors for PDB-related targets (backwards compat). + if (target.Contains("Pdb") && logger.ConfigurationErrorTargets.Contains(target)) + { + break; + } + + string actual = GetActualOutcome(logger, target); + string levelText = expectedLevel == FailureLevel.None + ? "any failure" + : expectedLevel.ToString(); + + sb.AppendLine( + $"Expected '{skimmer.Id}:{ruleName}' Fail (Level={levelText}) " + + $"for '{fileName}' but got: {actual}"); + } + + break; + } } } @@ -185,8 +203,16 @@ private void Verify( } Assert.Equal(0, sb.Length); - Assert.Empty(expected); - Assert.Empty(other); + } + + private static string GetActualOutcome(TestMessageLogger logger, string target) + { + if (logger.PassTargets.Contains(target)) return "Pass"; + if (logger.ErrorTargets.Contains(target)) return "Error"; + if (logger.WarningTargets.Contains(target)) return "Warning"; + if (logger.NoteTargets.Contains(target)) return "Note"; + if (logger.ConfigurationErrorTargets.Contains(target)) return "ConfigurationError"; + return "no result"; } private static string GetTestDirectoryFor(string ruleName) @@ -297,7 +323,6 @@ private void VerifyApplicability( HashSet applicabilityConditions, AnalysisApplicability expectedApplicability = AnalysisApplicability.NotApplicableToSpecifiedTarget, bool useDefaultPolicy = false, - bool bypassExtensionValidation = false, string expectedReasonForNotAnalyzing = null) { string ruleName = skimmer.GetType().Name; @@ -311,7 +336,7 @@ private void VerifyApplicability( { foreach (string target in Directory.GetFiles(testFilesDirectory, "*", SearchOption.AllDirectories)) { - if (bypassExtensionValidation || MultithreadedAnalyzeCommand.ValidAnalysisFileExtensions.Contains(Path.GetExtension(target))) + if (!ExcludedTestFileExtensions.Contains(Path.GetExtension(target))) { targets.Add(target); } @@ -548,7 +573,7 @@ public void BA2001_LoadImageAboveFourGigabyteAddress_Pass() [Fact] public void BA2001_LoadImageAboveFourGigabyteAddress_Fail() { - this.VerifyFail(new LoadImageAboveFourGigabyteAddress()); + this.Verify(new LoadImageAboveFourGigabyteAddress(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } [Fact] @@ -582,8 +607,10 @@ public void BA2002_DoNotIncorporateVulnerableDependencies_Fail() { if (BinaryParsers.PlatformSpecificHelpers.RunningOnWindows()) { - this.VerifyFail( + this.Verify( new DoNotIncorporateVulnerableDependencies(), + ExpectedOutcome.Fail, + expectedLevel: FailureLevel.Error, useDefaultPolicy: true); } else @@ -628,11 +655,37 @@ public void BA2004_EnableSecureSourceCodeHashing_Pass() } [Fact] - public void BA2004_EnableSecureSourceCodeHashing_Fail() + public void BA2004_EnableSecureSourceCodeHashing_Fail_Error() + { + if (BinaryParsers.PlatformSpecificHelpers.RunningOnWindows()) + { + // Managed assemblies with insecure hashing and native binaries with + // directly insecure object files emit Error. + this.Verify( + new EnableSecureSourceCodeHashing(), + ExpectedOutcome.Fail, + expectedLevel: FailureLevel.Error, + useDefaultPolicy: true, + outcomeSubdirectory: "Error"); + } + else + { + VerifyThrows(new DoNotDisableStackProtectionForFunctions(), useDefaultPolicy: true); + } + } + + [Fact] + public void BA2004_EnableSecureSourceCodeHashing_Fail_Warning() { if (BinaryParsers.PlatformSpecificHelpers.RunningOnWindows()) { - this.VerifyFail(new EnableSecureSourceCodeHashing(), useDefaultPolicy: true); + // Native binaries that link insecure static libraries emit Warning. + this.Verify( + new EnableSecureSourceCodeHashing(), + ExpectedOutcome.Fail, + expectedLevel: FailureLevel.Warning, + useDefaultPolicy: true, + outcomeSubdirectory: "Warning"); } else { @@ -645,7 +698,7 @@ public void BA2005_DoNotShipVulnerableBinaries_Fail() { if (BinaryParsers.PlatformSpecificHelpers.RunningOnWindows()) { - this.VerifyFail(new DoNotShipVulnerableBinaries(), useDefaultPolicy: true); + this.Verify(new DoNotShipVulnerableBinaries(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error, useDefaultPolicy: true); } else { @@ -704,8 +757,10 @@ public void BA2006_BuildWithSecureTools_Fail() { if (BinaryParsers.PlatformSpecificHelpers.RunningOnWindows()) { - this.VerifyFail( + this.Verify( new BuildWithSecureTools(), + ExpectedOutcome.Fail, + expectedLevel: FailureLevel.Error, useDefaultPolicy: true); } else @@ -745,8 +800,10 @@ public void BA2007_EnableCriticalCompilerWarnings_Fail() { if (BinaryParsers.PlatformSpecificHelpers.RunningOnWindows()) { - this.VerifyFail( + this.Verify( new EnableCriticalCompilerWarnings(), + ExpectedOutcome.Fail, + expectedLevel: FailureLevel.Error, useDefaultPolicy: true); } else @@ -794,8 +851,10 @@ public void BA2008_EnableControlFlowGuard_Pass() [Fact] public void BA2008_EnableControlFlowGuard_Fail() { - this.VerifyFail( + this.Verify( new EnableControlFlowGuard(), + ExpectedOutcome.Fail, + expectedLevel: FailureLevel.Error, useDefaultPolicy: true); } @@ -824,7 +883,7 @@ public void BA2009_EnableAddressSpaceLayoutRandomization_Pass() [Fact] public void BA2009_EnableAddressSpaceLayoutRandomization_Fail() { - this.VerifyFail(new EnableAddressSpaceLayoutRandomization()); + this.Verify(new EnableAddressSpaceLayoutRandomization(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } [Fact] @@ -849,7 +908,7 @@ public void BA2010_DoNotMarkImportsSectionAsExecutable_Pass() [Fact] public void BA2010_DoNotMarkImportsSectionAsExecutable_Fail() { - this.VerifyFail(new DoNotMarkImportsSectionAsExecutable()); + this.Verify(new DoNotMarkImportsSectionAsExecutable(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } [Fact] @@ -882,7 +941,7 @@ public void BA2011_EnableStackProtection_Fail() { if (BinaryParsers.PlatformSpecificHelpers.RunningOnWindows()) { - this.VerifyFail(new EnableStackProtection()); + this.Verify(new EnableStackProtection(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } else { @@ -907,7 +966,7 @@ public void BA2012_DoNotModifyStackProtectionCookie_Pass() [Fact] public void BA2012_DoNotModifyStackProtectionCookie_Fail() { - this.VerifyFail(new DoNotModifyStackProtectionCookie()); + this.Verify(new DoNotModifyStackProtectionCookie(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } [Fact] @@ -955,8 +1014,10 @@ public void BA2013_InitializeStackProtection_Fail() { if (BinaryParsers.PlatformSpecificHelpers.RunningOnWindows()) { - this.VerifyFail( + this.Verify( new InitializeStackProtection(), + ExpectedOutcome.Fail, + expectedLevel: FailureLevel.Error, useDefaultPolicy: true); } else @@ -1010,9 +1071,11 @@ public void BA2014_DoNotDisableStackProtectionForFunctions_Fail() MetadataConditions.CouldNotLoadPdb, }; - this.VerifyFail( + this.Verify( new DoNotDisableStackProtectionForFunctions(), - GetTestFilesMatchingConditions(failureConditions), + ExpectedOutcome.Fail, + expectedLevel: FailureLevel.Error, + additionalTestFiles: GetTestFilesMatchingConditions(failureConditions), useDefaultPolicy: true); } else @@ -1057,7 +1120,7 @@ public void BA2015_EnableHighEntropyVirtualAddresses_Pass() [Fact] public void BA2015_EnableHighEntropyVirtualAddresses_Fail() { - this.VerifyFail(new EnableHighEntropyVirtualAddresses()); + this.Verify(new EnableHighEntropyVirtualAddresses(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } [Fact] @@ -1082,7 +1145,7 @@ public void BA2016_MarkImageAsNXCompatible_Pass() [Fact] public void BA2016_MarkImageAsNXCompatible_Fail() { - this.VerifyFail(new MarkImageAsNXCompatible()); + this.Verify(new MarkImageAsNXCompatible(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } [Fact] @@ -1109,7 +1172,7 @@ public void BA2018_EnableSafeSEH_Pass() [Fact] public void BA2018_EnableSafeSEH_Fail() { - this.VerifyFail(new EnableSafeSEH()); + this.Verify(new EnableSafeSEH(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } [Fact] @@ -1134,7 +1197,7 @@ public void BA2019_DoNotMarkWritableSectionsAsShared_Pass() [Fact] public void BA2019_DoNotMarkWritableSectionsAsShared_Fail() { - this.VerifyFail(new DoNotMarkWritableSectionsAsShared()); + this.Verify(new DoNotMarkWritableSectionsAsShared(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } [Fact] @@ -1157,7 +1220,7 @@ public void BA2021_DoNotMarkWritableSectionsAsExecutable_Pass() [Fact] public void BA2021_DoNotMarkWritableSectionsAsExecutable_Fail() { - this.VerifyFail(new DoNotMarkWritableSectionsAsExecutable()); + this.Verify(new DoNotMarkWritableSectionsAsExecutable(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } [Fact] @@ -1177,7 +1240,7 @@ public void BA2022_SignSecurely_Fail() { if (BinaryParsers.PlatformSpecificHelpers.RunningOnWindows()) { - this.VerifyFail(new SignSecurely()); + this.Verify(new SignSecurely(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } else { @@ -1218,7 +1281,7 @@ public void BA2024_EnableSpectreMitigations_Fail() { if (BinaryParsers.PlatformSpecificHelpers.RunningOnWindows()) { - this.VerifyFail(new EnableSpectreMitigations(), useDefaultPolicy: true); + this.Verify(new EnableSpectreMitigations(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Warning, useDefaultPolicy: true); } } @@ -1236,7 +1299,7 @@ public void BA2025_EnableShadowStack_Fail() { if (BinaryParsers.PlatformSpecificHelpers.RunningOnWindows()) { - this.VerifyFail(new EnableShadowStack(), useDefaultPolicy: true); + this.Verify(new EnableShadowStack(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Warning, useDefaultPolicy: true); } } @@ -1263,7 +1326,7 @@ public void BA2026_EnableMicrosoftCompilerSdlSwitch_Fail() { if (BinaryParsers.PlatformSpecificHelpers.RunningOnWindows()) { - this.VerifyFail(new EnableMicrosoftCompilerSdlSwitch(), useDefaultPolicy: true); + this.Verify(new EnableMicrosoftCompilerSdlSwitch(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Warning, useDefaultPolicy: true); } } @@ -1297,7 +1360,7 @@ public void BA2027_EnableSourceLink_Fail() { if (BinaryParsers.PlatformSpecificHelpers.RunningOnWindows()) { - this.VerifyFail(new EnableSourceLink()); + this.Verify(new EnableSourceLink(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Warning); } } @@ -1340,13 +1403,13 @@ public void BA2029_EnableIntegrityCheck_NotApplicable() [Fact] public void BA3001_EnablePositionIndependentExecutable_Pass() { - this.VerifyPass(new EnablePositionIndependentExecutable(), bypassExtensionValidation: true); + this.VerifyPass(new EnablePositionIndependentExecutable()); } [Fact] public void BA3001_EnablePositionIndependentExecutable_Fail() { - this.VerifyFail(new EnablePositionIndependentExecutable(), bypassExtensionValidation: true); + this.Verify(new EnablePositionIndependentExecutable(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } [Fact] @@ -1358,13 +1421,13 @@ public void BA3001_EnablePositionIndependentExecutable_NotApplicable() [Fact] public void BA3002_DoNotMarkStackAsExecutable_Pass() { - this.VerifyPass(new DoNotMarkStackAsExecutable(), bypassExtensionValidation: true); + this.VerifyPass(new DoNotMarkStackAsExecutable()); } [Fact] public void BA3002_DoNotMarkStackAsExecutable_Fail() { - this.VerifyFail(new DoNotMarkStackAsExecutable(), bypassExtensionValidation: true); + this.Verify(new DoNotMarkStackAsExecutable(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } [Fact] @@ -1376,79 +1439,79 @@ public void BA3002_DoNotMarkStackAsExecutable_NotApplicable() [Fact] public void BA3003_EnableStackProtector_Pass() { - this.VerifyPass(new EnableStackProtector(), bypassExtensionValidation: true); + this.VerifyPass(new EnableStackProtector()); } [Fact] public void BA3003_EnableStackProtector_Fail() { - this.VerifyFail(new EnableStackProtector(), bypassExtensionValidation: true); + this.Verify(new EnableStackProtector(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } [Fact] public void BA3003_EnableStackProtector_NotApplicable() { - this.VerifyApplicability(new EnableStackProtector(), new HashSet(), bypassExtensionValidation: true); + this.VerifyApplicability(new EnableStackProtector(), new HashSet()); } [Fact] public void BA3004_GenerateRequiredSymbolFormat_Pass() { - this.VerifyPass(new GenerateRequiredSymbolFormat(), bypassExtensionValidation: true); + this.VerifyPass(new GenerateRequiredSymbolFormat()); } [Fact] public void BA3004_GenerateRequiredSymbolFormat_Fail() { - this.VerifyFail(new GenerateRequiredSymbolFormat(), bypassExtensionValidation: true); + this.Verify(new GenerateRequiredSymbolFormat(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } [Fact] public void BA3005_EnableStackClashProtection_Pass() { - this.VerifyPass(new EnableStackClashProtection(), bypassExtensionValidation: true); + this.VerifyPass(new EnableStackClashProtection()); } [Fact] public void BA3005_EnableStackClashProtection_Fail() { - this.VerifyFail(new EnableStackClashProtection(), bypassExtensionValidation: true); + this.Verify(new EnableStackClashProtection(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } [Fact] public void BA3005_EnableStackClashProtection_NotApplicable() { - this.VerifyApplicability(new EnableStackClashProtection(), new HashSet(), bypassExtensionValidation: true); + this.VerifyApplicability(new EnableStackClashProtection(), new HashSet()); } [Fact] public void BA3006_EnableNonExecutableStack_Pass() { - this.VerifyPass(new EnableNonExecutableStack(), bypassExtensionValidation: true); + this.VerifyPass(new EnableNonExecutableStack()); } [Fact] public void BA3006_EnableNonExecutableStack_Fail() { - this.VerifyFail(new EnableNonExecutableStack(), bypassExtensionValidation: true); + this.Verify(new EnableNonExecutableStack(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } [Fact] public void BA3006_EnableNonExecutableStack_NotApplicable() { - this.VerifyApplicability(new EnableNonExecutableStack(), new HashSet(), bypassExtensionValidation: true); + this.VerifyApplicability(new EnableNonExecutableStack(), new HashSet()); } [Fact] public void BA3010_EnableReadOnlyRelocations_Pass() { - this.VerifyPass(new EnableReadOnlyRelocations(), bypassExtensionValidation: true); + this.VerifyPass(new EnableReadOnlyRelocations()); } [Fact] public void BA3010_EnableReadOnlyRelocations_Fail() { - this.VerifyFail(new EnableReadOnlyRelocations(), bypassExtensionValidation: true); + this.Verify(new EnableReadOnlyRelocations(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } [Fact] @@ -1460,13 +1523,13 @@ public void BA3010_EnableReadOnlyRelocations_NotApplicable() [Fact] public void BA3011_EnableBindNow_Pass() { - this.VerifyPass(new EnableBindNow(), bypassExtensionValidation: true); + this.VerifyPass(new EnableBindNow()); } [Fact] public void BA3011_EnableBindNow_Fail() { - this.VerifyFail(new EnableBindNow(), bypassExtensionValidation: true); + this.Verify(new EnableBindNow(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } [Fact] @@ -1478,13 +1541,13 @@ public void BA3011_EnableBindNow_NotApplicable() [Fact] public void BA3030_UseGccCheckedFunctions_Pass() { - this.VerifyPass(new UseGccCheckedFunctions(), bypassExtensionValidation: true); + this.VerifyPass(new UseGccCheckedFunctions()); } [Fact] public void BA3030_UseGccCheckedFunctions_Fail() { - this.VerifyFail(new UseGccCheckedFunctions(), bypassExtensionValidation: true); + this.Verify(new UseGccCheckedFunctions(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } [Fact] @@ -1496,13 +1559,13 @@ public void BA3030_UseGccCheckedFunctions_NotApplicable() [Fact] public void BA3031_EnableClangSafeStack_Pass() { - this.VerifyPass(new EnableClangSafeStack(), bypassExtensionValidation: true); + this.VerifyPass(new EnableClangSafeStack()); } [Fact] public void BA3031_EnableClangSafeStack_Fail() { - this.VerifyFail(new EnableClangSafeStack(), bypassExtensionValidation: true); + this.Verify(new EnableClangSafeStack(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } [Fact] @@ -1514,13 +1577,13 @@ public void BA3031_EnableClangSafeStack_NotApplicable() [Fact] public void BA5001_EnablePositionIndependentExecutableMachO_Pass() { - this.VerifyPass(new EnablePositionIndependentExecutableMachO(), bypassExtensionValidation: true); + this.VerifyPass(new EnablePositionIndependentExecutableMachO()); } [Fact] public void BA5001_EnablePositionIndependentExecutableMachO_Fail() { - this.VerifyFail(new EnablePositionIndependentExecutableMachO(), bypassExtensionValidation: true); + this.Verify(new EnablePositionIndependentExecutableMachO(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } [Fact] @@ -1532,13 +1595,13 @@ public void BA5001_EnablePositionIndependentExecutableMachO_NotApplicable() [Fact] public void BA5002_DoNotAllowExecutableStack_Pass() { - this.VerifyPass(new DoNotAllowExecutableStack(), bypassExtensionValidation: true); + this.VerifyPass(new DoNotAllowExecutableStack()); } [Fact] public void BA5002_DoNotAllowExecutableStack_Fail() { - this.VerifyFail(new DoNotAllowExecutableStack(), bypassExtensionValidation: true); + this.Verify(new DoNotAllowExecutableStack(), ExpectedOutcome.Fail, expectedLevel: FailureLevel.Error); } [Fact] @@ -1552,15 +1615,10 @@ public void BA6001_DisableIncrementalLinkingInReleaseBuilds_Fail() { if (BinaryParsers.PlatformSpecificHelpers.RunningOnWindows()) { - // Every PDB parsing rule should return an error if a PDB can't be located. - // Be sure to delete this code (and remove passing the 'failureConditions` - // arguments to 'VerifyFail' if not implementing a PDB crawling check. - var failureConditions = new HashSet - { - MetadataConditions.CouldNotLoadPdb - }; - this.VerifyFail( + this.Verify( new DisableIncrementalLinkingInReleaseBuilds(), + ExpectedOutcome.Fail, + expectedLevel: FailureLevel.Warning, useDefaultPolicy: true); } else @@ -1597,15 +1655,10 @@ public void BA6002_EliminateDuplicateStrings_Fail() { if (BinaryParsers.PlatformSpecificHelpers.RunningOnWindows()) { - // Every PDB parsing rule should return an error if a PDB can't be located. - // Be sure to delete this code (and remove passing the 'failureConditions` - // arguments to 'VerifyFail' if not implementing a PDB crawling check. - var failureConditions = new HashSet - { - MetadataConditions.CouldNotLoadPdb - }; - this.VerifyFail( + this.Verify( new EliminateDuplicateStrings(), + ExpectedOutcome.Fail, + expectedLevel: FailureLevel.Warning, useDefaultPolicy: true); } else @@ -1642,15 +1695,10 @@ public void BA6004_EnableCOMDATFolding_Fail() { if (BinaryParsers.PlatformSpecificHelpers.RunningOnWindows()) { - // Every PDB parsing rule should return an error if a PDB can't be located. - // Be sure to delete this code (and remove passing the 'failureConditions` - // arguments to 'VerifyFail' if not implementing a PDB crawling check. - var failureConditions = new HashSet - { - MetadataConditions.CouldNotLoadPdb - }; - this.VerifyFail( + this.Verify( new EnableComdatFolding(), + ExpectedOutcome.Fail, + expectedLevel: FailureLevel.Warning, useDefaultPolicy: true); } else @@ -1687,15 +1735,10 @@ public void BA6005_EnableOptimizeReferences_Fail() { if (BinaryParsers.PlatformSpecificHelpers.RunningOnWindows()) { - // Every PDB parsing rule should return an error if a PDB can't be located. - // Be sure to delete this code (and remove passing the 'failureConditions` - // arguments to 'VerifyFail' if not implementing a PDB crawling check. - var failureConditions = new HashSet - { - MetadataConditions.CouldNotLoadPdb - }; - this.VerifyFail( + this.Verify( new EnableOptimizeReferences(), + ExpectedOutcome.Fail, + expectedLevel: FailureLevel.Warning, useDefaultPolicy: true); } else @@ -1732,15 +1775,10 @@ public void BA6006_EnableLinkTimeCodeGeneration_Fail() { if (BinaryParsers.PlatformSpecificHelpers.RunningOnWindows()) { - // Every PDB parsing rule should return an error if a PDB can't be located. - // Be sure to delete this code (and remove passing the 'failureConditions` - // arguments to 'VerifyFail' if not implementing a PDB crawling check. - var failureConditions = new HashSet - { - MetadataConditions.CouldNotLoadPdb - }; - this.VerifyFail( + this.Verify( new EnableLinkTimeCodeGeneration(), + ExpectedOutcome.Fail, + expectedLevel: FailureLevel.Warning, useDefaultPolicy: true); } else From ec9a8106802cefe12a64c05582538f3c25306b58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20=C5=A0vendov=C3=A1?= Date: Thu, 4 Jun 2026 15:14:50 +0200 Subject: [PATCH 16/36] Add portable executable and PDB files for .NET Core and .NET Standard - Introduced Managed_netcoreapp3.1_portable_exe_sha1.pdb to support secure source code hashing tests. - Added Managed_netstandard2.0_portable_sha1.dll and its corresponding PDB file for .NET Standard 2.0 to enhance testing capabilities. --- .../Native_x64_VS2022_WithPCH_sha1.exe | Bin .../Native_x64_VS2022_WithPCH_sha1.pdb | Bin .../Managed_net48_full_exe_sha1.exe | Bin .../Managed_net48_full_exe_sha1.pdb | Bin .../{Error => Warning}/Managed_net48_full_sha1.dll | Bin .../{Error => Warning}/Managed_net48_full_sha1.pdb | Bin .../Managed_net48_portable_exe_sha1.exe | Bin .../Managed_net48_portable_exe_sha1.pdb | Bin .../Managed_net48_portable_sha1.dll | Bin .../Managed_net48_portable_sha1.pdb | Bin .../Managed_netcoreapp3.1_portable_exe_sha1.dll | Bin .../Managed_netcoreapp3.1_portable_exe_sha1.pdb | Bin .../Managed_netstandard2.0_portable_sha1.dll | Bin .../Managed_netstandard2.0_portable_sha1.pdb | Bin src/Test.FunctionalTests.BinSkim.Rules/RuleTests.cs | 2 +- 15 files changed, 1 insertion(+), 1 deletion(-) rename src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/{Warning => Error}/Native_x64_VS2022_WithPCH_sha1.exe (100%) rename src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/{Warning => Error}/Native_x64_VS2022_WithPCH_sha1.pdb (100%) rename src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/{Error => Warning}/Managed_net48_full_exe_sha1.exe (100%) rename src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/{Error => Warning}/Managed_net48_full_exe_sha1.pdb (100%) rename src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/{Error => Warning}/Managed_net48_full_sha1.dll (100%) rename src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/{Error => Warning}/Managed_net48_full_sha1.pdb (100%) rename src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/{Error => Warning}/Managed_net48_portable_exe_sha1.exe (100%) rename src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/{Error => Warning}/Managed_net48_portable_exe_sha1.pdb (100%) rename src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/{Error => Warning}/Managed_net48_portable_sha1.dll (100%) rename src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/{Error => Warning}/Managed_net48_portable_sha1.pdb (100%) rename src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/{Error => Warning}/Managed_netcoreapp3.1_portable_exe_sha1.dll (100%) rename src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/{Error => Warning}/Managed_netcoreapp3.1_portable_exe_sha1.pdb (100%) rename src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/{Error => Warning}/Managed_netstandard2.0_portable_sha1.dll (100%) rename src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/{Error => Warning}/Managed_netstandard2.0_portable_sha1.pdb (100%) diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Native_x64_VS2022_WithPCH_sha1.exe b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Native_x64_VS2022_WithPCH_sha1.exe similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Native_x64_VS2022_WithPCH_sha1.exe rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Native_x64_VS2022_WithPCH_sha1.exe diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Native_x64_VS2022_WithPCH_sha1.pdb b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Native_x64_VS2022_WithPCH_sha1.pdb similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Native_x64_VS2022_WithPCH_sha1.pdb rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Native_x64_VS2022_WithPCH_sha1.pdb diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_net48_full_exe_sha1.exe b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_net48_full_exe_sha1.exe similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_net48_full_exe_sha1.exe rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_net48_full_exe_sha1.exe diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_net48_full_exe_sha1.pdb b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_net48_full_exe_sha1.pdb similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_net48_full_exe_sha1.pdb rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_net48_full_exe_sha1.pdb diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_net48_full_sha1.dll b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_net48_full_sha1.dll similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_net48_full_sha1.dll rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_net48_full_sha1.dll diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_net48_full_sha1.pdb b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_net48_full_sha1.pdb similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_net48_full_sha1.pdb rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_net48_full_sha1.pdb diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_net48_portable_exe_sha1.exe b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_net48_portable_exe_sha1.exe similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_net48_portable_exe_sha1.exe rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_net48_portable_exe_sha1.exe diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_net48_portable_exe_sha1.pdb b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_net48_portable_exe_sha1.pdb similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_net48_portable_exe_sha1.pdb rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_net48_portable_exe_sha1.pdb diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_net48_portable_sha1.dll b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_net48_portable_sha1.dll similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_net48_portable_sha1.dll rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_net48_portable_sha1.dll diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_net48_portable_sha1.pdb b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_net48_portable_sha1.pdb similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_net48_portable_sha1.pdb rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_net48_portable_sha1.pdb diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_netcoreapp3.1_portable_exe_sha1.dll b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_netcoreapp3.1_portable_exe_sha1.dll similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_netcoreapp3.1_portable_exe_sha1.dll rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_netcoreapp3.1_portable_exe_sha1.dll diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_netcoreapp3.1_portable_exe_sha1.pdb b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_netcoreapp3.1_portable_exe_sha1.pdb similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_netcoreapp3.1_portable_exe_sha1.pdb rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_netcoreapp3.1_portable_exe_sha1.pdb diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_netstandard2.0_portable_sha1.dll b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_netstandard2.0_portable_sha1.dll similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_netstandard2.0_portable_sha1.dll rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_netstandard2.0_portable_sha1.dll diff --git a/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_netstandard2.0_portable_sha1.pdb b/src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_netstandard2.0_portable_sha1.pdb similarity index 100% rename from src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Error/Managed_netstandard2.0_portable_sha1.pdb rename to src/Test.FunctionalTests.BinSkim.Rules/FunctionalTestData/BA2004.EnableSecureSourceCodeHashing/Fail/Warning/Managed_netstandard2.0_portable_sha1.pdb diff --git a/src/Test.FunctionalTests.BinSkim.Rules/RuleTests.cs b/src/Test.FunctionalTests.BinSkim.Rules/RuleTests.cs index 88b71856..c984541a 100644 --- a/src/Test.FunctionalTests.BinSkim.Rules/RuleTests.cs +++ b/src/Test.FunctionalTests.BinSkim.Rules/RuleTests.cs @@ -58,7 +58,7 @@ private void VerifyFail( useDefaultPolicy: useDefaultPolicy); } - private static readonly HashSet ExcludedTestFileExtensions = new HashSet(StringComparer.OrdinalIgnoreCase) { ".pdb" }; + private static readonly HashSet ExcludedTestFileExtensions = new HashSet(StringComparer.OrdinalIgnoreCase) { ".pdb", ".ilk", ".txt", ".dsym" }; private void Verify( BinarySkimmer skimmer, From d053d62fcb540c3c0bfdfadea1f299066e37d2a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20=C5=A0vendov=C3=A1?= Date: Thu, 4 Jun 2026 16:14:27 +0200 Subject: [PATCH 17/36] Refactor LEB128 encoding helpers into DwarfTestHelpers for code reuse in unit tests --- .../Dwarf/DwarfCompilationUnitFormTests.cs | 20 +------ .../Dwarf/DwarfLineNumberProgramTests.cs | 46 +-------------- .../Dwarf/DwarfSymbolProviderTests.cs | 21 +------ .../Dwarf/DwarfTestHelpers.cs | 58 +++++++++++++++++++ 4 files changed, 62 insertions(+), 83 deletions(-) create mode 100644 src/Test.UnitTests.BinaryParsers/Dwarf/DwarfTestHelpers.cs diff --git a/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfCompilationUnitFormTests.cs b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfCompilationUnitFormTests.cs index a18441b9..73181041 100644 --- a/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfCompilationUnitFormTests.cs +++ b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfCompilationUnitFormTests.cs @@ -46,25 +46,7 @@ private class StubDwarfBinary : IDwarfBinary public void Dispose() { } } - /// - /// Helper to encode an unsigned LEB128 value into bytes. - /// - private static byte[] EncodeULEB128(ulong value) - { - var bytes = new List(); - do - { - byte b = (byte)(value & 0x7F); - value >>= 7; - if (value != 0) - { - b |= 0x80; - } - - bytes.Add(b); - } while (value != 0); - return bytes.ToArray(); - } + private static byte[] EncodeULEB128(ulong value) => DwarfTestHelpers.EncodeULEB128(value); /// /// Builds a DWARF5 32-bit compilation unit header for .debug_info. diff --git a/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfLineNumberProgramTests.cs b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfLineNumberProgramTests.cs index 0e16a641..94b3ef21 100644 --- a/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfLineNumberProgramTests.cs +++ b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfLineNumberProgramTests.cs @@ -19,26 +19,7 @@ namespace Microsoft.CodeAnalysis.BinaryParsers.Dwarf /// public class DwarfLineNumberProgramTests { - /// - /// Helper to encode an unsigned LEB128 value into bytes. - /// Copied from DwarfCompilationUnitFormTests to keep tests self-contained. - /// - private static byte[] EncodeULEB128(ulong value) - { - var bytes = new List(); - do - { - byte b = (byte)(value & 0x7F); - value >>= 7; - if (value != 0) - { - b |= 0x80; - } - - bytes.Add(b); - } while (value != 0); - return bytes.ToArray(); - } + private static byte[] EncodeULEB128(ulong value) => DwarfTestHelpers.EncodeULEB128(value); [Fact] public void Constructor_WithUnitLengthLessOrEqualToOne_ReturnsNullFiles() @@ -270,30 +251,7 @@ public void ReadData_Dwarf5_DirectoriesAndFiles_UseStrpAndDirectoryIndex() #region Helpers - /// - /// Encode a signed LEB128 value into bytes. - /// - private static byte[] EncodeSLEB128(int value) - { - var bytes = new List(); - bool more = true; - while (more) - { - byte b = (byte)(value & 0x7F); - value >>= 7; - if ((value == 0 && (b & 0x40) == 0) || (value == -1 && (b & 0x40) != 0)) - { - more = false; - } - else - { - b |= 0x80; - } - - bytes.Add(b); - } - return bytes.ToArray(); - } + private static byte[] EncodeSLEB128(int value) => DwarfTestHelpers.EncodeSLEB128(value); /// /// Builds a minimal DWARF4 .debug_line section and returns the parsed program. diff --git a/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfSymbolProviderTests.cs b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfSymbolProviderTests.cs index 2a9c04d6..cbdaafa5 100644 --- a/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfSymbolProviderTests.cs +++ b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfSymbolProviderTests.cs @@ -47,26 +47,7 @@ private class StubDwarfBinary : IDwarfBinary public void Dispose() { } } - /// - /// Helper to encode an unsigned LEB128 value into bytes. - /// Copied from DwarfCompilationUnitFormTests / DwarfLineNumberProgramTests to keep tests self-contained. - /// - private static byte[] EncodeULEB128(ulong value) - { - var bytes = new List(); - do - { - byte b = (byte)(value & 0x7F); - value >>= 7; - if (value != 0) - { - b |= 0x80; - } - - bytes.Add(b); - } while (value != 0); - return bytes.ToArray(); - } + private static byte[] EncodeULEB128(ulong value) => DwarfTestHelpers.EncodeULEB128(value); // ---- ParseDebugStringOffsets ---- diff --git a/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfTestHelpers.cs b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfTestHelpers.cs new file mode 100644 index 00000000..2f034e13 --- /dev/null +++ b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfTestHelpers.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; + +namespace Microsoft.CodeAnalysis.BinaryParsers.Dwarf +{ + /// + /// Shared encoding helpers for DWARF unit tests. + /// + internal static class DwarfTestHelpers + { + /// + /// Encodes an unsigned LEB128 value into bytes. + /// + internal static byte[] EncodeULEB128(ulong value) + { + var bytes = new List(); + do + { + byte b = (byte)(value & 0x7F); + value >>= 7; + if (value != 0) + { + b |= 0x80; + } + + bytes.Add(b); + } while (value != 0); + return bytes.ToArray(); + } + + /// + /// Encodes a signed LEB128 value into bytes. + /// + internal static byte[] EncodeSLEB128(int value) + { + var bytes = new List(); + bool more = true; + while (more) + { + byte b = (byte)(value & 0x7F); + value >>= 7; + if ((value == 0 && (b & 0x40) == 0) || (value == -1 && (b & 0x40) != 0)) + { + more = false; + } + else + { + b |= 0x80; + } + + bytes.Add(b); + } + return bytes.ToArray(); + } + } +} From 4379ef3cbc41e44c42029159db1576b621b5b01f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20=C5=A0vendov=C3=A1?= Date: Mon, 8 Jun 2026 14:11:49 +0200 Subject: [PATCH 18/36] Refactor integration tests: remove redundant tests, add repository root finder, and update project configuration --- docs/BinSkimRules.md | 8 +-- .../AnalyzeCommandIntegrationTests.cs | 72 +++++++------------ .../BinSkimRunner.cs | 11 ++- ...est.IntegrationTests.BinSkim.Driver.csproj | 6 ++ .../xunit.runner.json | 5 ++ 5 files changed, 50 insertions(+), 52 deletions(-) create mode 100644 src/Test.IntegrationTests.BinSkim.Driver/xunit.runner.json diff --git a/docs/BinSkimRules.md b/docs/BinSkimRules.md index cf3dbc51..4c1f003f 100644 --- a/docs/BinSkimRules.md +++ b/docs/BinSkimRules.md @@ -309,7 +309,7 @@ For VC projects make sure that ItemDefinitionGroup - Link - BaseAddress property #### `Pass`: Pass -'{0}' is a 64-bit image with a base address that is >= 4 gigabytes, increasing the effectiveness of Address Space Layout Randomization (which helps prevent attackers from executing security-sensitive code in well-known locations). +'{0}' is a 64-bit image with a base address that is >= 4 gigabytes, increasing the effectiveness of Address Space Layout Randomization (which helps prevent attackers from executing security-sensitive code in well-known locations). #### `Error`: Error @@ -356,7 +356,7 @@ This information is typically used to resolve source file when debugging but it This validation is helpful in verifying supply chain integrity. Due to this security focus, it is important that the hashing algorithm used to produce checksums is secure. Legacy hashing algorithms, such as MD5 and SHA-1, have been demonstrated to be broken by modern hardware (that is, it is computationally feasible to force hash collisions, in which a common hash is generated from distinct files). Using a secure hashing algorithm, such as SHA-256, prevents the possibility of collision attacks, in which the checksum of a malicious file is used to produce a hash that satisfies the system that it is, in fact, the original file processed by the compiler. -For managed binaries, pass '-checksumalgorithm:SHA256' on the csc.exe command-line or populate the '<ChecksumAlgorithm>' project property with 'SHA256' to enable secure source code hashing. +For managed binaries, pass '-checksumalgorithm:SHA256' on the csc.exe command-line or populate the '' project property with 'SHA256' to enable secure source code hashing. For native code - use to MSVC 17.0 (14.30.*) or later if possible. For VC projects use PlatformToolset property with 'v143' or later value. When using older MSVC versions add /ZH:SHA_256 on cl.exe command line. @@ -376,7 +376,7 @@ The following modules are out of policy: #### `Managed`: Error -'{0}' is a managed binary compiled with an insecure ({1}) source code hashing algorithm. {1} is subject to collision attacks and its use can compromise supply chain integrity. Pass '-checksumalgorithm:SHA256' on the csc.exe command-line or populate the project <ChecksumAlgorithm> property with 'SHA256' to enable secure source code hashing. +'{0}' is a managed binary compiled with an insecure ({1}) source code hashing algorithm. {1} is subject to collision attacks and its use can compromise supply chain integrity. Pass '-checksumalgorithm:SHA256' on the csc.exe command-line or populate the project property with 'SHA256' to enable secure source code hashing. #### `NativeWithInsecureDirectCompilands`: Error @@ -895,7 +895,7 @@ Images should be correctly signed by trusted publishers using cryptographically ### Description -Application code which stores sensitive data in memory should be compiled with the Spectre mitigations switch (/Qspectre cl.exe command-line argument or <SpectreMitigation>Spectre</SpectreMitigation> build property). +Application code which stores sensitive data in memory should be compiled with the Spectre mitigations switch (/Qspectre cl.exe command-line argument or Spectre build property). Spectre attacks can compromise hardware-based isolation, allowing non-privileged users to retrieve potentially sensitive data from the CPU cache. To resolve this issue, ensure that all modules compiled into the binary are compiled with /Qspectre switch on cl.exe command-line. You may need to install the 'C++ spectre-mitigated libs' component from the Visual Studio installer if you observe violations against C runtime libraries such as libcmt.lib, libvcruntime.lib, etc. diff --git a/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs b/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs index 5a02aaf3..177e8445 100644 --- a/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs +++ b/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs @@ -31,24 +31,6 @@ public void Dispose() try { Directory.Delete(_tempDir, recursive: true); } catch { /* best-effort cleanup */ } } - [Fact] - public async Task Analyze_SelfScan_ExitsWithZero() - { - // BinSkim analyzing its own DLL — PDB is co-located so symbol loading should succeed. - string targetBinary = BinSkimRunner.GetBinSkimDllPath(); - string sarifOutput = Path.Combine(_tempDir, "output.sarif"); - - BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] - { - "analyze", - targetBinary, - "-o", sarifOutput, - }); - - result.ExitCode.Should().Be(0, - $"BinSkim should exit cleanly.\nStdOut: {result.StdOut}\nStdErr: {result.StdErr}"); - } - [Fact] public async Task Analyze_SelfScan_ProducesValidSarif() { @@ -278,16 +260,35 @@ public async Task ExportConfig_ProducesValidJsonOutput() json.Should().NotBeNullOrWhiteSpace("config file should contain content"); } + /// + /// Finds the repository root by searching upward for the BinSkim.sln file. + /// + private static string FindRepoRoot() + { + string dir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + while (dir != null) + { + if (File.Exists(Path.Combine(dir, "src", "BinSkim.sln"))) + { + return dir; + } + + dir = Path.GetDirectoryName(dir); + } + + throw new DirectoryNotFoundException( + "Could not locate the repository root (searched upward for src/BinSkim.sln from assembly location)."); + } + /// /// Resolves a path into the BinSkim.Rules functional test data directory. /// These binaries are curated per-rule with known Pass/Fail outcomes. /// private static string GetFunctionalTestDataPath(params string[] relativeParts) { - string assemblyDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); - string testDataRoot = Path.GetFullPath( - Path.Combine(assemblyDir, "..", "..", "..", "..", "src", - "Test.FunctionalTests.BinSkim.Rules", "FunctionalTestData")); + string repoRoot = FindRepoRoot(); + string testDataRoot = Path.Combine(repoRoot, "src", + "Test.FunctionalTests.BinSkim.Rules", "FunctionalTestData"); string fullPath = Path.Combine(new[] { testDataRoot }.Concat(relativeParts).ToArray()); if (!File.Exists(fullPath) && !Directory.Exists(fullPath)) @@ -304,10 +305,9 @@ private static string GetFunctionalTestDataPath(params string[] relativeParts) /// private static string GetBinaryParsersTestDataPath(params string[] relativeParts) { - string assemblyDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); - string testDataRoot = Path.GetFullPath( - Path.Combine(assemblyDir, "..", "..", "..", "..", "src", - "Test.UnitTests.BinaryParsers", "TestData")); + string repoRoot = FindRepoRoot(); + string testDataRoot = Path.Combine(repoRoot, "src", + "Test.UnitTests.BinaryParsers", "TestData"); string fullPath = Path.Combine(new[] { testDataRoot }.Concat(relativeParts).ToArray()); if (!File.Exists(fullPath) && !Directory.Exists(fullPath)) @@ -321,26 +321,6 @@ private static string GetBinaryParsersTestDataPath(params string[] relativeParts #region ELF Binary Analysis - [Fact] - public async Task Analyze_ElfBinary_ExitsWithZero() - { - string elfBinary = GetFunctionalTestDataPath( - "BA3006.EnableNonExecutableStack", "Pass", "gcc.helloworld.noexecstack.5.o"); - string sarifOutput = Path.Combine(_tempDir, "elf-output.sarif"); - - BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] - { - "analyze", - elfBinary, - "-o", sarifOutput, - "--kind", "Fail;Pass;NotApplicable", - "--level", "Error;Warning;Note", - }); - - result.ExitCode.Should().Be(0, - $"BinSkim should analyze ELF binaries successfully.\nStdOut: {result.StdOut}\nStdErr: {result.StdErr}"); - } - [Fact] public async Task Analyze_ElfBinary_ProducesValidSarif() { diff --git a/src/Test.IntegrationTests.BinSkim.Driver/BinSkimRunner.cs b/src/Test.IntegrationTests.BinSkim.Driver/BinSkimRunner.cs index 10559e79..4a0d7950 100644 --- a/src/Test.IntegrationTests.BinSkim.Driver/BinSkimRunner.cs +++ b/src/Test.IntegrationTests.BinSkim.Driver/BinSkimRunner.cs @@ -58,11 +58,18 @@ public static string GetBinSkimDllPath() string binSkimDll = Path.GetFullPath( Path.Combine(assemblyDir, "..", "..", "BinSkim.Driver", "release", "BinSkim.dll")); + if (!File.Exists(binSkimDll)) + { + // Fall back to debug build. + binSkimDll = Path.GetFullPath( + Path.Combine(assemblyDir, "..", "..", "BinSkim.Driver", "debug", "BinSkim.dll")); + } + if (!File.Exists(binSkimDll)) { throw new FileNotFoundException( - $"BinSkim.dll not found at expected path: {binSkimDll}. " + - "Ensure the BinSkim.Driver project has been built in Release configuration."); + $"BinSkim.dll not found at expected path (checked both release and debug): {binSkimDll}. " + + "Ensure the BinSkim.Driver project has been built."); } return binSkimDll; diff --git a/src/Test.IntegrationTests.BinSkim.Driver/Test.IntegrationTests.BinSkim.Driver.csproj b/src/Test.IntegrationTests.BinSkim.Driver/Test.IntegrationTests.BinSkim.Driver.csproj index 1e836d59..734ff0f2 100644 --- a/src/Test.IntegrationTests.BinSkim.Driver/Test.IntegrationTests.BinSkim.Driver.csproj +++ b/src/Test.IntegrationTests.BinSkim.Driver/Test.IntegrationTests.BinSkim.Driver.csproj @@ -5,6 +5,9 @@ $(NetCoreVersion) Library True + + Integration @@ -17,6 +20,9 @@ + + + diff --git a/src/Test.IntegrationTests.BinSkim.Driver/xunit.runner.json b/src/Test.IntegrationTests.BinSkim.Driver/xunit.runner.json new file mode 100644 index 00000000..9c92449e --- /dev/null +++ b/src/Test.IntegrationTests.BinSkim.Driver/xunit.runner.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", + "methodDisplay": "classAndMethod", + "diagnosticMessages": true +} From 07b5447f7a99007a079d6a0f19cde3dd58bce94a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20=C5=A0vendov=C3=A1?= Date: Mon, 8 Jun 2026 14:48:15 +0200 Subject: [PATCH 19/36] Fix applicability condition for EnableShadowStack in RuleTests --- docs/BinSkimRules.md | 8 ++++---- src/Test.FunctionalTests.BinSkim.Rules/RuleTests.cs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/BinSkimRules.md b/docs/BinSkimRules.md index 4c1f003f..cf3dbc51 100644 --- a/docs/BinSkimRules.md +++ b/docs/BinSkimRules.md @@ -309,7 +309,7 @@ For VC projects make sure that ItemDefinitionGroup - Link - BaseAddress property #### `Pass`: Pass -'{0}' is a 64-bit image with a base address that is >= 4 gigabytes, increasing the effectiveness of Address Space Layout Randomization (which helps prevent attackers from executing security-sensitive code in well-known locations). +'{0}' is a 64-bit image with a base address that is >= 4 gigabytes, increasing the effectiveness of Address Space Layout Randomization (which helps prevent attackers from executing security-sensitive code in well-known locations). #### `Error`: Error @@ -356,7 +356,7 @@ This information is typically used to resolve source file when debugging but it This validation is helpful in verifying supply chain integrity. Due to this security focus, it is important that the hashing algorithm used to produce checksums is secure. Legacy hashing algorithms, such as MD5 and SHA-1, have been demonstrated to be broken by modern hardware (that is, it is computationally feasible to force hash collisions, in which a common hash is generated from distinct files). Using a secure hashing algorithm, such as SHA-256, prevents the possibility of collision attacks, in which the checksum of a malicious file is used to produce a hash that satisfies the system that it is, in fact, the original file processed by the compiler. -For managed binaries, pass '-checksumalgorithm:SHA256' on the csc.exe command-line or populate the '' project property with 'SHA256' to enable secure source code hashing. +For managed binaries, pass '-checksumalgorithm:SHA256' on the csc.exe command-line or populate the '<ChecksumAlgorithm>' project property with 'SHA256' to enable secure source code hashing. For native code - use to MSVC 17.0 (14.30.*) or later if possible. For VC projects use PlatformToolset property with 'v143' or later value. When using older MSVC versions add /ZH:SHA_256 on cl.exe command line. @@ -376,7 +376,7 @@ The following modules are out of policy: #### `Managed`: Error -'{0}' is a managed binary compiled with an insecure ({1}) source code hashing algorithm. {1} is subject to collision attacks and its use can compromise supply chain integrity. Pass '-checksumalgorithm:SHA256' on the csc.exe command-line or populate the project property with 'SHA256' to enable secure source code hashing. +'{0}' is a managed binary compiled with an insecure ({1}) source code hashing algorithm. {1} is subject to collision attacks and its use can compromise supply chain integrity. Pass '-checksumalgorithm:SHA256' on the csc.exe command-line or populate the project <ChecksumAlgorithm> property with 'SHA256' to enable secure source code hashing. #### `NativeWithInsecureDirectCompilands`: Error @@ -895,7 +895,7 @@ Images should be correctly signed by trusted publishers using cryptographically ### Description -Application code which stores sensitive data in memory should be compiled with the Spectre mitigations switch (/Qspectre cl.exe command-line argument or Spectre build property). +Application code which stores sensitive data in memory should be compiled with the Spectre mitigations switch (/Qspectre cl.exe command-line argument or <SpectreMitigation>Spectre</SpectreMitigation> build property). Spectre attacks can compromise hardware-based isolation, allowing non-privileged users to retrieve potentially sensitive data from the CPU cache. To resolve this issue, ensure that all modules compiled into the binary are compiled with /Qspectre switch on cl.exe command-line. You may need to install the 'C++ spectre-mitigated libs' component from the Visual Studio installer if you observe violations against C runtime libraries such as libcmt.lib, libvcruntime.lib, etc. diff --git a/src/Test.FunctionalTests.BinSkim.Rules/RuleTests.cs b/src/Test.FunctionalTests.BinSkim.Rules/RuleTests.cs index c984541a..0a8c8290 100644 --- a/src/Test.FunctionalTests.BinSkim.Rules/RuleTests.cs +++ b/src/Test.FunctionalTests.BinSkim.Rules/RuleTests.cs @@ -1317,7 +1317,7 @@ public void BA2025_EnableShadowStack_NotApplicable() this.VerifyApplicabililtyByConditionsOnly( skimmer: new EnableShadowStack(), - applicabilityConditions: notApplicableArm64, + applicabilityConditions: notApplicableArm, expectedReasonForNotAnalyzing: MetadataConditions.ImageIsArmBinary); } From 19bc249b5140219556241d0bee5a2cc494f48b58 Mon Sep 17 00:00:00 2001 From: Sasinkas Date: Mon, 8 Jun 2026 20:41:44 +0200 Subject: [PATCH 20/36] Potential fix for pull request finding 'CodeQL / Call to 'System.IO.Path.Combine' may silently drop its earlier arguments' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .../AnalyzeCommandIntegrationTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs b/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs index 177e8445..d9cd4a5d 100644 --- a/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs +++ b/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs @@ -22,7 +22,7 @@ public class AnalyzeCommandIntegrationTests : IDisposable public AnalyzeCommandIntegrationTests() { - _tempDir = Path.Combine(Path.GetTempPath(), "BinSkimIntegration", Guid.NewGuid().ToString("N")); + _tempDir = Path.Join(Path.GetTempPath(), "BinSkimIntegration", Guid.NewGuid().ToString("N")); Directory.CreateDirectory(_tempDir); } From de65664286ef1fadfc14bed16aed978a71776d97 Mon Sep 17 00:00:00 2001 From: Sasinkas Date: Mon, 8 Jun 2026 20:42:10 +0200 Subject: [PATCH 21/36] Potential fix for pull request finding 'CodeQL / Call to 'System.IO.Path.Combine' may silently drop its earlier arguments' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .../AnalyzeCommandIntegrationTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs b/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs index d9cd4a5d..d1a0eb59 100644 --- a/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs +++ b/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs @@ -35,7 +35,7 @@ public void Dispose() public async Task Analyze_SelfScan_ProducesValidSarif() { string targetBinary = BinSkimRunner.GetBinSkimDllPath(); - string sarifOutput = Path.Combine(_tempDir, "output.sarif"); + string sarifOutput = Path.Join(_tempDir, "output.sarif"); BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] { From 3ba606a58bfbdf6578685449ec000e8f80fc8b62 Mon Sep 17 00:00:00 2001 From: Sasinkas Date: Mon, 8 Jun 2026 20:42:58 +0200 Subject: [PATCH 22/36] Potential fix for pull request finding 'CodeQL / Call to 'System.IO.Path.Combine' may silently drop its earlier arguments' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .../AnalyzeCommandIntegrationTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs b/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs index d1a0eb59..06a1e6df 100644 --- a/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs +++ b/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs @@ -112,8 +112,8 @@ public async Task Analyze_RunOnlyRules_FiltersToSpecifiedRule() [Fact] public async Task Analyze_NoValidTargets_ExitsWithNonZero() { - string nonExistentTarget = Path.Combine(_tempDir, "does_not_exist.dll"); - string sarifOutput = Path.Combine(_tempDir, "output.sarif"); + string nonExistentTarget = Path.Join(_tempDir, "does_not_exist.dll"); + string sarifOutput = Path.Join(_tempDir, "output.sarif"); BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] { From 7f9b15b65e53937770b92b0c36cf7065a9001a66 Mon Sep 17 00:00:00 2001 From: Sasinkas Date: Mon, 8 Jun 2026 20:43:20 +0200 Subject: [PATCH 23/36] Potential fix for pull request finding 'CodeQL / Call to 'System.IO.Path.Combine' may silently drop its earlier arguments' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- src/Test.IntegrationTests.BinSkim.Driver/BinSkimRunner.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Test.IntegrationTests.BinSkim.Driver/BinSkimRunner.cs b/src/Test.IntegrationTests.BinSkim.Driver/BinSkimRunner.cs index 4a0d7950..9cbd5e12 100644 --- a/src/Test.IntegrationTests.BinSkim.Driver/BinSkimRunner.cs +++ b/src/Test.IntegrationTests.BinSkim.Driver/BinSkimRunner.cs @@ -56,13 +56,13 @@ public static string GetBinSkimDllPath() { string assemblyDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); string binSkimDll = Path.GetFullPath( - Path.Combine(assemblyDir, "..", "..", "BinSkim.Driver", "release", "BinSkim.dll")); + Path.Join(assemblyDir, "..", "..", "BinSkim.Driver", "release", "BinSkim.dll")); if (!File.Exists(binSkimDll)) { // Fall back to debug build. binSkimDll = Path.GetFullPath( - Path.Combine(assemblyDir, "..", "..", "BinSkim.Driver", "debug", "BinSkim.dll")); + Path.Join(assemblyDir, "..", "..", "BinSkim.Driver", "debug", "BinSkim.dll")); } if (!File.Exists(binSkimDll)) From faf29ea6caa66e8ee069855461de1a38af93d979 Mon Sep 17 00:00:00 2001 From: Sasinkas Date: Mon, 8 Jun 2026 20:43:48 +0200 Subject: [PATCH 24/36] Potential fix for pull request finding 'CodeQL / Call to 'System.IO.Path.Combine' may silently drop its earlier arguments' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .../AnalyzeCommandIntegrationTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs b/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs index 06a1e6df..d8aa8177 100644 --- a/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs +++ b/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs @@ -57,7 +57,7 @@ public async Task Analyze_KnownFailBinary_ProducesErrorResults() { string failBinary = GetFunctionalTestDataPath( "BA2016.MarkImageAsNXCompatible", "Fail", "ManagedFail.dll"); - string sarifOutput = Path.Combine(_tempDir, "fail-output.sarif"); + string sarifOutput = Path.Join(_tempDir, "fail-output.sarif"); BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] { From 3ebe1f76fd9e9a140bb3ac2f1eabdd9db11d1b6e Mon Sep 17 00:00:00 2001 From: Sasinkas Date: Mon, 8 Jun 2026 20:44:44 +0200 Subject: [PATCH 25/36] Potential fix for pull request finding 'CodeQL / Call to 'System.IO.Path.Combine' may silently drop its earlier arguments' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> From b84a2110043850ae634e8da67e0d1749c67af648 Mon Sep 17 00:00:00 2001 From: Sasinkas Date: Mon, 8 Jun 2026 20:47:18 +0200 Subject: [PATCH 26/36] Potential fix for pull request finding 'CodeQL / Call to 'System.IO.Path.Combine' may silently drop its earlier arguments' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .../AnalyzeCommandIntegrationTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs b/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs index d8aa8177..c33f8dcc 100644 --- a/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs +++ b/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs @@ -222,7 +222,7 @@ public async Task Dump_Verbose_ProducesMoreDetailedOutput() [Fact] public async Task ExportRules_ProducesValidSarifOutput() { - string outputPath = Path.Combine(_tempDir, "rules.sarif"); + string outputPath = Path.Join(_tempDir, "rules.sarif"); BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] { From d2eb70f8ecef8e8e7d06873301bf51ab725d57b1 Mon Sep 17 00:00:00 2001 From: Sasinkas Date: Mon, 8 Jun 2026 20:47:44 +0200 Subject: [PATCH 27/36] Potential fix for pull request finding 'CodeQL / Call to 'System.IO.Path.Combine' may silently drop its earlier arguments' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .../AnalyzeCommandIntegrationTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs b/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs index c33f8dcc..0cf97bda 100644 --- a/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs +++ b/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs @@ -84,7 +84,7 @@ public async Task Analyze_KnownFailBinary_ProducesErrorResults() public async Task Analyze_RunOnlyRules_FiltersToSpecifiedRule() { string targetBinary = BinSkimRunner.GetBinSkimDllPath(); - string sarifOutput = Path.Combine(_tempDir, "filtered.sarif"); + string sarifOutput = Path.Join(_tempDir, "filtered.sarif"); BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] { From bbc3d5d4bfc1de2bc60e1174f4be788b4c0f175e Mon Sep 17 00:00:00 2001 From: Sasinkas Date: Tue, 9 Jun 2026 10:36:45 +0200 Subject: [PATCH 28/36] Potential fix for pull request finding 'CodeQL / Call to 'System.IO.Path.Combine' may silently drop its earlier arguments' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .../AnalyzeCommandIntegrationTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs b/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs index 0cf97bda..56f9af16 100644 --- a/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs +++ b/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs @@ -287,9 +287,9 @@ private static string FindRepoRoot() private static string GetFunctionalTestDataPath(params string[] relativeParts) { string repoRoot = FindRepoRoot(); - string testDataRoot = Path.Combine(repoRoot, "src", + string testDataRoot = Path.Join(repoRoot, "src", "Test.FunctionalTests.BinSkim.Rules", "FunctionalTestData"); - string fullPath = Path.Combine(new[] { testDataRoot }.Concat(relativeParts).ToArray()); + string fullPath = Path.Join(new[] { testDataRoot }.Concat(relativeParts).ToArray()); if (!File.Exists(fullPath) && !Directory.Exists(fullPath)) { @@ -306,9 +306,9 @@ private static string GetFunctionalTestDataPath(params string[] relativeParts) private static string GetBinaryParsersTestDataPath(params string[] relativeParts) { string repoRoot = FindRepoRoot(); - string testDataRoot = Path.Combine(repoRoot, "src", + string testDataRoot = Path.Join(repoRoot, "src", "Test.UnitTests.BinaryParsers", "TestData"); - string fullPath = Path.Combine(new[] { testDataRoot }.Concat(relativeParts).ToArray()); + string fullPath = Path.Join(new[] { testDataRoot }.Concat(relativeParts).ToArray()); if (!File.Exists(fullPath) && !Directory.Exists(fullPath)) { From 8db7b495b1f02c992b0e035762db7d7a2c1cdb90 Mon Sep 17 00:00:00 2001 From: Sasinkas Date: Tue, 9 Jun 2026 10:38:43 +0200 Subject: [PATCH 29/36] Potential fix for pull request finding 'CodeQL / Call to 'System.IO.Path.Combine' may silently drop its earlier arguments' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .../AnalyzeCommandIntegrationTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs b/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs index 56f9af16..412e0abf 100644 --- a/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs +++ b/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs @@ -243,7 +243,7 @@ public async Task ExportRules_ProducesValidSarifOutput() [Fact] public async Task ExportConfig_ProducesValidJsonOutput() { - string outputPath = Path.Combine(_tempDir, "config.json"); + string outputPath = Path.Join(_tempDir, "config.json"); BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] { From bee3aa2db0b35d49922c492967215ece9d94d1c8 Mon Sep 17 00:00:00 2001 From: Sasinkas Date: Tue, 9 Jun 2026 10:39:12 +0200 Subject: [PATCH 30/36] Potential fix for pull request finding 'CodeQL / Call to 'System.IO.Path.Combine' may silently drop its earlier arguments' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .../AnalyzeCommandIntegrationTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs b/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs index 412e0abf..492299ac 100644 --- a/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs +++ b/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs @@ -326,7 +326,7 @@ public async Task Analyze_ElfBinary_ProducesValidSarif() { string elfBinary = GetFunctionalTestDataPath( "BA3001.EnablePositionIndependentExecutable", "Pass", "gcc.pie_executable"); - string sarifOutput = Path.Combine(_tempDir, "elf-sarif.sarif"); + string sarifOutput = Path.Join(_tempDir, "elf-sarif.sarif"); BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] { From 8649118f9f8aadfb1e8fec3be4e3c9bf16f43b3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20=C5=A0vendov=C3=A1?= Date: Tue, 9 Jun 2026 10:43:11 +0200 Subject: [PATCH 31/36] Refactor variable declarations for clarity in Dwarf-related unit tests --- .../RuleTests.cs | 30 +++++++++-- .../Dwarf/DwarfCommonInformationEntryTests.cs | 8 +-- .../Dwarf/DwarfCompilationUnitFormTests.cs | 6 +-- .../Dwarf/DwarfLineNumberProgramTests.cs | 54 ++++++++++--------- 4 files changed, 62 insertions(+), 36 deletions(-) diff --git a/src/Test.FunctionalTests.BinSkim.Rules/RuleTests.cs b/src/Test.FunctionalTests.BinSkim.Rules/RuleTests.cs index 0a8c8290..bc6cf1cc 100644 --- a/src/Test.FunctionalTests.BinSkim.Rules/RuleTests.cs +++ b/src/Test.FunctionalTests.BinSkim.Rules/RuleTests.cs @@ -207,11 +207,31 @@ private void Verify( private static string GetActualOutcome(TestMessageLogger logger, string target) { - if (logger.PassTargets.Contains(target)) return "Pass"; - if (logger.ErrorTargets.Contains(target)) return "Error"; - if (logger.WarningTargets.Contains(target)) return "Warning"; - if (logger.NoteTargets.Contains(target)) return "Note"; - if (logger.ConfigurationErrorTargets.Contains(target)) return "ConfigurationError"; + if (logger.PassTargets.Contains(target)) + { + return "Pass"; + } + + if (logger.ErrorTargets.Contains(target)) + { + return "Error"; + } + + if (logger.WarningTargets.Contains(target)) + { + return "Warning"; + } + + if (logger.NoteTargets.Contains(target)) + { + return "Note"; + } + + if (logger.ConfigurationErrorTargets.Contains(target)) + { + return "ConfigurationError"; + } + return "no result"; } diff --git a/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfCommonInformationEntryTests.cs b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfCommonInformationEntryTests.cs index 0ebe83f5..33e9015c 100644 --- a/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfCommonInformationEntryTests.cs +++ b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfCommonInformationEntryTests.cs @@ -54,7 +54,7 @@ public void ParseAll_WithNonEmptyAugmentation_UsesFixedDefaultsAndReadsInstructi DwarfCommonInformationEntry[] entries = DwarfCommonInformationEntry.ParseAll(reader, defaultAddressSize: 8); entries.Should().HaveCount(1); - var entry = entries[0]; + DwarfCommonInformationEntry entry = entries[0]; entry.Version.Should().Be(3); entry.Augmentation.Should().Be("x"); @@ -115,7 +115,7 @@ public void ParseAll_WithEmptyAugmentationAndVersionLessThanFour_UsesDefaultAddr DwarfCommonInformationEntry[] entries = DwarfCommonInformationEntry.ParseAll(reader, defaultAddressSize: 8); entries.Should().HaveCount(1); - var entry = entries[0]; + DwarfCommonInformationEntry entry = entries[0]; entry.Version.Should().Be(3); entry.Augmentation.Should().BeEmpty(); @@ -180,7 +180,7 @@ public void ParseAll_WithEmptyAugmentationAndVersionFour_ReadsExplicitAddressAnd DwarfCommonInformationEntry[] entries = DwarfCommonInformationEntry.ParseAll(reader, defaultAddressSize: 4); entries.Should().HaveCount(1); - var entry = entries[0]; + DwarfCommonInformationEntry entry = entries[0]; entry.Version.Should().Be(4); entry.Augmentation.Should().BeEmpty(); @@ -280,7 +280,7 @@ public void ParseAll_WithFdeFollowingCie_AttachesFrameDescriptionEntryAndParsesA DwarfCommonInformationEntry[] entries = DwarfCommonInformationEntry.ParseAll(reader, defaultAddressSize: 8); entries.Should().HaveCount(1); - var entry = entries[0]; + DwarfCommonInformationEntry entry = entries[0]; entry.AddressSize.Should().Be(8); entry.FrameDescriptionEntries.Should().HaveCount(1); diff --git a/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfCompilationUnitFormTests.cs b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfCompilationUnitFormTests.cs index 73181041..1971d9a5 100644 --- a/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfCompilationUnitFormTests.cs +++ b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfCompilationUnitFormTests.cs @@ -1143,13 +1143,13 @@ public void ReadData_InsertsVoidBaseTypeAndDefaultsPointerTypeToVoid() stub.NormalizeAddress); cu.SymbolsTree.Should().HaveCount(1); - var pointer = cu.SymbolsTree[0]; + DwarfSymbol pointer = cu.SymbolsTree[0]; pointer.Tag.Should().Be(DwarfTag.PointerType); // Synthetic void base type is inserted as first child. pointer.Children.Should().NotBeNull(); pointer.Children.Should().NotBeEmpty(); - var voidSymbol = pointer.Children[0]; + DwarfSymbol voidSymbol = pointer.Children[0]; voidSymbol.Tag.Should().Be(DwarfTag.BaseType); voidSymbol.Attributes.Should().ContainKey(DwarfAttribute.Name); @@ -1167,7 +1167,7 @@ public void ReadData_InsertsVoidBaseTypeAndDefaultsPointerTypeToVoid() // And receives a Type attribute that resolves to the synthetic void symbol. pointer.Attributes.Should().ContainKey(DwarfAttribute.Type); - var typeAttr = pointer.Attributes[DwarfAttribute.Type]; + DwarfAttributeValue typeAttr = pointer.Attributes[DwarfAttribute.Type]; typeAttr.Type.Should().Be(DwarfAttributeValueType.ResolvedReference); typeAttr.Reference.Should().BeSameAs(voidSymbol); diff --git a/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfLineNumberProgramTests.cs b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfLineNumberProgramTests.cs index 94b3ef21..6fec6001 100644 --- a/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfLineNumberProgramTests.cs +++ b/src/Test.UnitTests.BinaryParsers/Dwarf/DwarfLineNumberProgramTests.cs @@ -114,11 +114,11 @@ public void ReadData_Dwarf4_SingleFileSingleCopy_ProducesOneNormalizedLine() program.Files.Should().NotBeNull(); program.Files.Should().HaveCount(1); - var file = program.Files[0]; + DwarfFileInformation file = program.Files[0]; file.Name.Should().Be("file.c"); file.Lines.Should().HaveCount(1); - var line = file.Lines[0]; + DwarfLineInformation line = file.Lines[0]; line.File.Should().BeSameAs(file); line.Line.Should().Be(1u); // initial line line.Column.Should().Be(0ul); // default column @@ -235,7 +235,7 @@ public void ReadData_Dwarf5_DirectoriesAndFiles_UseStrpAndDirectoryIndex() program.Files.Should().NotBeNull(); program.Files.Should().HaveCount(1); - var file = program.Files[0]; + DwarfFileInformation file = program.Files[0]; file.Name.Should().Be("foo.c"); file.Directory.Should().Be("dir"); // Path combines directory and name; we only check that both segments appear. @@ -243,7 +243,7 @@ public void ReadData_Dwarf5_DirectoriesAndFiles_UseStrpAndDirectoryIndex() file.Path.Should().Contain("dir"); file.Lines.Should().HaveCount(1); - var line = file.Lines[0]; + DwarfLineInformation line = file.Lines[0]; line.File.Should().BeSameAs(file); line.Line.Should().Be(1u); line.Address.Should().Be(0u); @@ -382,7 +382,10 @@ public void ReadData_WithVersionLessThanTwo_ReturnsNull() bw.Write((byte)0); // lineBase bw.Write((byte)1); // lineRange bw.Write((byte)13); // opcodeBase - for (int i = 1; i < 13; i++) bw.Write((byte)0); + for (int i = 1; i < 13; i++) + { + bw.Write((byte)0); + } } byte[] bodyBytes = body.ToArray(); @@ -445,7 +448,7 @@ public void ReadData_WithEndPositionBeyondBuffer_ReturnsNull() [Fact] public void ReadData_AdvancePc_AdvancesAddressByOperandTimesMinInstructionLength() { - var program = BuildDwarf4Program(bw => + DwarfLineNumberProgram program = BuildDwarf4Program(bw => { bw.Write((byte)DwarfLineNumberStandardOpcode.AdvancePc); bw.Write(EncodeULEB128(5)); @@ -460,7 +463,7 @@ public void ReadData_AdvancePc_AdvancesAddressByOperandTimesMinInstructionLength [Fact] public void ReadData_AdvanceLine_PositiveValue_IncrementsLine() { - var program = BuildDwarf4Program(bw => + DwarfLineNumberProgram program = BuildDwarf4Program(bw => { bw.Write((byte)DwarfLineNumberStandardOpcode.AdvanceLine); bw.Write(EncodeSLEB128(10)); @@ -474,7 +477,7 @@ public void ReadData_AdvanceLine_PositiveValue_IncrementsLine() [Fact] public void ReadData_AdvanceLine_NegativeAfterPositive_DecrementsLine() { - var program = BuildDwarf4Program(bw => + DwarfLineNumberProgram program = BuildDwarf4Program(bw => { bw.Write((byte)DwarfLineNumberStandardOpcode.AdvanceLine); bw.Write(EncodeSLEB128(20)); @@ -492,7 +495,7 @@ public void ReadData_AdvanceLine_NegativeAfterPositive_DecrementsLine() [Fact] public void ReadData_SetFile_SwitchesToSecondFile() { - var program = BuildDwarf4Program(bw => + DwarfLineNumberProgram program = BuildDwarf4Program(bw => { // SetFile to file 2 (1-based index) bw.Write((byte)DwarfLineNumberStandardOpcode.SetFile); @@ -509,7 +512,7 @@ public void ReadData_SetFile_SwitchesToSecondFile() [Fact] public void ReadData_SetColumn_SetsColumnOnEmittedLine() { - var program = BuildDwarf4Program(bw => + DwarfLineNumberProgram program = BuildDwarf4Program(bw => { bw.Write((byte)DwarfLineNumberStandardOpcode.SetColumn); bw.Write(EncodeULEB128(42)); @@ -524,7 +527,7 @@ public void ReadData_ConstAddPc_AdvancesAddressByFormula() { // With opcodeBase=13, lineRange=14, minInstrLen=1: // advance = (255 - 13) / 14 = 17 - var program = BuildDwarf4Program(bw => + DwarfLineNumberProgram program = BuildDwarf4Program(bw => { bw.Write((byte)DwarfLineNumberStandardOpcode.ConstAddPc); bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); @@ -536,7 +539,7 @@ public void ReadData_ConstAddPc_AdvancesAddressByFormula() [Fact] public void ReadData_FixedAdvancePc_AddsUshortToAddress() { - var program = BuildDwarf4Program(bw => + DwarfLineNumberProgram program = BuildDwarf4Program(bw => { bw.Write((byte)DwarfLineNumberStandardOpcode.FixedAdvancePc); bw.Write((ushort)256); @@ -553,7 +556,7 @@ public void ReadData_FixedAdvancePc_AddsUshortToAddress() [Fact] public void ReadData_SetAddress_SetsAddressToValue() { - var program = BuildDwarf4Program(bw => + DwarfLineNumberProgram program = BuildDwarf4Program(bw => { WriteSetAddress(bw, 0x2000); bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); @@ -565,7 +568,7 @@ public void ReadData_SetAddress_SetsAddressToValue() [Fact] public void ReadData_EndSequence_EmitsLineAndResetsState() { - var program = BuildDwarf4Program(bw => + DwarfLineNumberProgram program = BuildDwarf4Program(bw => { bw.Write((byte)DwarfLineNumberStandardOpcode.AdvanceLine); bw.Write(EncodeSLEB128(5)); @@ -587,7 +590,7 @@ public void ReadData_EndSequence_EmitsLineAndResetsState() [Fact] public void ReadData_SetAddress_WhenZero_UsesLastAddressFromPreviousEndSequence() { - var program = BuildDwarf4Program(bw => + DwarfLineNumberProgram program = BuildDwarf4Program(bw => { WriteSetAddress(bw, 0x5000); WriteEndSequence(bw); @@ -603,7 +606,7 @@ public void ReadData_SetAddress_WhenZero_UsesLastAddressFromPreviousEndSequence( [Fact] public void ReadData_DefineFile_AddsAndSwitchesToNewFile() { - var program = BuildDwarf4Program(bw => + DwarfLineNumberProgram program = BuildDwarf4Program(bw => { // Extended: DefineFile bw.Write((byte)0x00); // extended marker @@ -637,7 +640,7 @@ public void ReadData_SpecialOpcode_AdvancesAddressAndLine() // operationAdvance = 7 / 12 = 0 // lineAdvance = -3 + (7 % 12) = -3 + 7 = 4 // Result: address=0, line=1+4=5 - var program = BuildDwarf4Program(bw => + DwarfLineNumberProgram program = BuildDwarf4Program(bw => { bw.Write((byte)20); // special opcode }, lineBase: -3, lineRange: 12); @@ -655,7 +658,7 @@ public void ReadData_SpecialOpcode_WithAddressAdvance() // operationAdvance = 8 / 4 = 2 // lineAdvance = 0 + (8 % 4) = 0 // address = 2 * 2 = 4, line = 1 - var program = BuildDwarf4Program(bw => + DwarfLineNumberProgram program = BuildDwarf4Program(bw => { bw.Write((byte)21); }, lineBase: 0, lineRange: 4, minimumInstructionLength: 2); @@ -670,7 +673,7 @@ public void ReadData_MultipleSpecialOpcodes_AccumulateState() // lineBase=0, lineRange=4, opcodeBase=13, minInstrLen=1 // Opcode 17: adjusted=4, opAdvance=1, lineAdv=0. addr=1, line=1. // Opcode 17: adjusted=4, opAdvance=1, lineAdv=0. addr=2, line=1. - var program = BuildDwarf4Program(bw => + DwarfLineNumberProgram program = BuildDwarf4Program(bw => { bw.Write((byte)17); bw.Write((byte)17); @@ -688,7 +691,7 @@ public void ReadData_MultipleSpecialOpcodes_AccumulateState() [Fact] public void ReadData_Dwarf4_MultipleDirectories_ResolvesFilePaths() { - var program = BuildDwarf4Program(bw => + DwarfLineNumberProgram program = BuildDwarf4Program(bw => { bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); }, fileNames: null, directories: new[] { "src", "lib" }); @@ -707,7 +710,10 @@ public void ReadData_Dwarf4_MultipleDirectories_ResolvesFilePaths() bw.Write((byte)0); bw.Write((byte)1); bw.Write((byte)13); - for (int i = 1; i < 13; i++) bw.Write((byte)0); + for (int i = 1; i < 13; i++) + { + bw.Write((byte)0); + } // Directories bw.Write(System.Text.Encoding.UTF8.GetBytes("src")); @@ -754,7 +760,7 @@ public void ReadData_Dwarf4_MultipleDirectories_ResolvesFilePaths() [Fact] public void ReadData_Dwarf4_MultipleFiles_LinesAssignedToCorrectFile() { - var program = BuildDwarf4Program(bw => + DwarfLineNumberProgram program = BuildDwarf4Program(bw => { bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); // line on file 1 bw.Write((byte)DwarfLineNumberStandardOpcode.SetFile); @@ -769,7 +775,7 @@ public void ReadData_Dwarf4_MultipleFiles_LinesAssignedToCorrectFile() [Fact] public void ReadData_NormalizationAppliedToAllLinesAcrossFiles() { - var program = BuildDwarf4Program(bw => + DwarfLineNumberProgram program = BuildDwarf4Program(bw => { WriteSetAddress(bw, 0x100); bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); @@ -788,7 +794,7 @@ public void ReadData_NormalizationAppliedToAllLinesAcrossFiles() public void ReadData_Dwarf3_ParsesWithoutMaxOperationsPerInstruction() { // DWARF3: no maxOperationsPerInstruction field in header. - var program = BuildDwarf4Program(bw => + DwarfLineNumberProgram program = BuildDwarf4Program(bw => { bw.Write((byte)DwarfLineNumberStandardOpcode.Copy); }, dwarfVersion: 3); From d0b9b5e32ad6f33e516fb6a7999ef50697a07db3 Mon Sep 17 00:00:00 2001 From: Sasinkas Date: Tue, 9 Jun 2026 10:44:35 +0200 Subject: [PATCH 32/36] Potential fix for pull request finding 'CodeQL / Call to 'System.IO.Path.Combine' may silently drop its earlier arguments' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .../AnalyzeCommandIntegrationTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs b/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs index 492299ac..8a4d7ffd 100644 --- a/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs +++ b/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs @@ -535,7 +535,7 @@ public async Task Analyze_LocalSymbolDirectories_AcceptsOption() public async Task Analyze_TraceTargetsScanned_ProducesTraceOutput() { string targetBinary = BinSkimRunner.GetBinSkimDllPath(); - string sarifOutput = Path.Combine(_tempDir, "trace.sarif"); + string sarifOutput = Path.Join(_tempDir, "trace.sarif"); BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] { From a0ff8f1addf0868dbbc88936dd3d69f2d7c731e7 Mon Sep 17 00:00:00 2001 From: Sasinkas Date: Tue, 9 Jun 2026 10:45:02 +0200 Subject: [PATCH 33/36] Potential fix for pull request finding 'CodeQL / Call to 'System.IO.Path.Combine' may silently drop its earlier arguments' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .../AnalyzeCommandIntegrationTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs b/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs index 8a4d7ffd..b977fc14 100644 --- a/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs +++ b/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs @@ -458,7 +458,7 @@ public async Task Analyze_Recurse_FindsNestedBinaries() BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] { "analyze", - Path.Combine(_tempDir, "*.dll"), + Path.Join(_tempDir, "*.dll"), "-o", sarifOutput, "--recurse", "True", "--kind", "Fail;Pass;NotApplicable", From 87205ec5b2e388e9426eab18de090698896531ce Mon Sep 17 00:00:00 2001 From: Sasinkas Date: Tue, 9 Jun 2026 10:45:24 +0200 Subject: [PATCH 34/36] Potential fix for pull request finding 'CodeQL / Call to 'System.IO.Path.Combine' may silently drop its earlier arguments' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .../AnalyzeCommandIntegrationTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs b/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs index b977fc14..291df678 100644 --- a/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs +++ b/src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs @@ -511,7 +511,7 @@ public async Task Analyze_RichReturnCode_InvalidTarget_ReturnsRuntimeConditions( public async Task Analyze_LocalSymbolDirectories_AcceptsOption() { string elfBinary = GetBinaryParsersTestDataPath("Dwarf", "hello-dwarf4-o2"); - string sarifOutput = Path.Combine(_tempDir, "symdir.sarif"); + string sarifOutput = Path.Join(_tempDir, "symdir.sarif"); BinSkimRunResult result = await BinSkimRunner.RunAsync(new[] { From 15f676c16b0e0d4e9861af26a553a107e093f790 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20=C5=A0vendov=C3=A1?= Date: Tue, 9 Jun 2026 10:51:23 +0200 Subject: [PATCH 35/36] Remove outdated documentation and notes from the memory bank --- .github/03-feature.instructions.md | 17 ----------- .memory-bank/activeContext.md | 49 ------------------------------ .memory-bank/learnings.md | 20 ------------ 3 files changed, 86 deletions(-) delete mode 100644 .memory-bank/activeContext.md delete mode 100644 .memory-bank/learnings.md diff --git a/.github/03-feature.instructions.md b/.github/03-feature.instructions.md index 13ef1d67..7245660f 100644 --- a/.github/03-feature.instructions.md +++ b/.github/03-feature.instructions.md @@ -3,22 +3,5 @@ applyTo: "**" --- ## Feature Instructions -Add unit tests to binskim class: -DwarfAttributeValue -DwarfCommonInformationEntry -DwarfCompilationUnit -DwarfFrameDescriptionEntry -DwarfLineNumberProgram -DwarfMemoryReader -DwarfSymbol -DwarfProvider -ElfBinary -Nice to have -guardian integration tests -guardian pipeline - run guardian with binskim on 1ESPT -Binskim Integration tests -pester tests (powershell) -Get rid of Verify Methods -do the tests less noisy - show only errors-warnings-exceptions? ## Testing Configuration diff --git a/.memory-bank/activeContext.md b/.memory-bank/activeContext.md deleted file mode 100644 index 42430964..00000000 --- a/.memory-bank/activeContext.md +++ /dev/null @@ -1,49 +0,0 @@ -# Active Context - -## Current Work Focus -BinSkim Integration Tests - Tracks 1-2 complete, Tracks 3-4 ready. - -## Recent Changes -- ✅ Track 1: Project scaffolding, BinSkimRunner helper, 5 initial tests -- ✅ Track 2: Core integration tests - 8 new tests (analyze verbs, dump, export-rules, export-config, error handling) -- Build: 0 warnings, 0 errors; Tests: 13/13 passing - -## Active Decisions -- Invocation: dotnet BinSkim.dll for cross-platform (Linux + Windows) -- BinSkim.Driver referenced with ReferenceOutputAssembly=false (build-order dependency only) -- Path resolution: navigate from test assembly up to bld/bin/BinSkim.Driver/release/BinSkim.dll -- Self-scan pattern: BinSkim analyzing its own DLL (PDB co-located) -- CommandLineParser writes help/version to stderr, tests check combined output -- BinSkim exits 0 even when rules fire errors. Exit code reflects tool health, not rule results -- Verb names: analyze, dump, export-rules, export-config (NOT export-rules-metadata/export-configuration) -- export-rules/export-config take positional output path arg (not --output) - -## Files Created/Modified -- NEW: src/Test.IntegrationTests.BinSkim.Driver/Test.IntegrationTests.BinSkim.Driver.csproj -- NEW: src/Test.IntegrationTests.BinSkim.Driver/BinSkimRunner.cs -- NEW: src/Test.IntegrationTests.BinSkim.Driver/AnalyzeCommandIntegrationTests.cs -- MOD: src/BinSkim.sln (added project + build configs) - -## Test Inventory (13 tests, all passing) -### Track 1 (original) -1. Analyze_SelfScan_ExitsWithZero -2. Analyze_SelfScan_ProducesValidSarif -3. Analyze_NoValidTargets_ExitsWithNonZero -4. Analyze_HelpFlag_ExitsCleanly -5. Analyze_VersionFlag_ExitsCleanly - -### Track 2 (new) -6. Analyze_KnownFailBinary_ProducesErrorResults - BA2016 fires error on ManagedFail.dll -7. Analyze_RunOnlyRules_FiltersToSpecifiedRule - --run-only-rules BA2016 filters results -8. Analyze_InvalidArgument_ExitsWithNonZero - --bogus-flag gives non-zero -9. Analyze_InvalidVerb_ExitsWithNonZero - unrecognized verb gives non-zero -10. Dump_SelfScan_ProducesMetadataOutput - dump outputs binary metadata -11. Dump_Verbose_ProducesMoreDetailedOutput - --verbose produces >= normal output -12. ExportRules_ProducesValidSarifOutput - export-rules creates .sarif with rule BA2016 -13. ExportConfig_ProducesValidJsonOutput - export-config creates .json config - -## Next Steps (Tracks 3-4 from plan) -1. ☐ Track 3: CLI Behavior Tests - response files, --recurse, config files, error handling - -## Current State -Tracks 1-2 complete. 13 integration tests all green. Ready for Tracks 3-4. diff --git a/.memory-bank/learnings.md b/.memory-bank/learnings.md deleted file mode 100644 index 6ede4968..00000000 --- a/.memory-bank/learnings.md +++ /dev/null @@ -1,20 +0,0 @@ -# Learnings - -- Initialized learnings file; no project-specific learnings recorded yet. -- DwarfAttributeValue represents DWARF attribute values with a Type enum (DwarfAttributeValueType) and a boxed Value, plus an optional Offset used for deferred resolution. -- Equality in DwarfAttributeValue is type-sensitive and uses special handling for some enum values (Address, Constant, Reference, SecOffset, Block, ExpressionLocation, Flag, String); other enum values currently fall through to a default “equal” path regardless of Value, which is important to keep in mind when adding tests. -- DwarfCompilationUnit maps DWARF4 forms (Data*, Address, Block*, String/Strp, Flag*, Ref*, ExpressionLocation, SecOffset, etc.) and DWARF5/extended forms (LineStrp, StrpSup, Strx*/GNUStrIndex, Addrx*/GNUAddrIndex, Rnglistx, Loclistx, RefSup*, RefSig8) into DwarfAttributeValue instances by setting Type, Value, and/or Offset according to the DWARF spec and LLVM’s DWARFFormValue behavior. -## Guardian / 1ES Infrastructure -- "Guardian" in this repo refers to Microsoft 1ES SDL infrastructure agent pools (guardian-build-infra-windows-x64, guardian-build-infra-linux-x64), NOT a separate tool or NuGet package. -- 1ESPT (1 Engineering System Perf and Test) is the compliance scanning pool (pool-1espt-mseng). -- Pipeline templates come from 1ESPipelineTemplates/1ESPipelineTemplates repo (refs/tags/release): Official template for build, Unofficial for compliance. -- Compliance pipeline runs BinSkim@4 (v4.3.1), CodeQL, CredScan@2, SPMI on net9.0 release outputs. -- Build pipeline uses custom NuGet feed (BinSkim.Build on mseng/1ES), validates release PRs are merged, triggers internal BinSkimInternal pipeline (ID 21327). -- No .gdnconfig/.gdn guardian config files exist; all config is in pipeline YAML. - -## Test Infrastructure -- Functional tests use xUnit + FluentAssertions on .NET 9 (netcoreapp9.0). -- Test.FunctionalTests.BinSkim.Driver uses baseline SARIF comparison (Expected/ vs NonWindowsExpected/ folders). -- Test.FunctionalTests.BinSkim.Rules uses Pass/Fail binary folders per rule (BAXXX.RuleFriendlyName pattern). -- Test assets include PE (exe/dll), ELF, Mach-O binaries from various compilers. -- UpdateBaselines.ps1 / .sh scripts regenerate expected SARIF outputs. \ No newline at end of file From 1335ba7fcbf61a17166212ded48c7b6f113e7517 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20=C5=A0vendov=C3=A1?= Date: Fri, 12 Jun 2026 11:27:07 +0200 Subject: [PATCH 36/36] Add blank lines for improved readability in feature instructions --- .github/03-feature.instructions.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/03-feature.instructions.md b/.github/03-feature.instructions.md index 7245660f..2f4d2547 100644 --- a/.github/03-feature.instructions.md +++ b/.github/03-feature.instructions.md @@ -4,4 +4,6 @@ applyTo: "**" ## Feature Instructions + + ## Testing Configuration