Add property-based tests for SBOM/VEX document ordering and Unicode normalization determinism
- Implement `SbomVexOrderingDeterminismProperties` for testing component list and vulnerability metadata hash consistency. - Create `UnicodeNormalizationDeterminismProperties` to validate NFC normalization and Unicode string handling. - Add project file for `StellaOps.Testing.Determinism.Properties` with necessary dependencies. - Introduce CI/CD template validation tests including YAML syntax checks and documentation content verification. - Create validation script for CI/CD templates ensuring all required files and structures are present.
This commit is contained in:
@@ -0,0 +1,509 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// FeatureExtractorTests.cs
|
||||
// Sprint: SPRINT_20251226_011_BINIDX_known_build_catalog
|
||||
// Task: BINCAT-17 - Unit tests for identity extraction (ELF, PE, Mach-O)
|
||||
// Description: Unit tests for binary feature extraction across all formats
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using FluentAssertions;
|
||||
using StellaOps.BinaryIndex.Core.Models;
|
||||
using StellaOps.BinaryIndex.Core.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.BinaryIndex.Core.Tests;
|
||||
|
||||
public class ElfFeatureExtractorTests
|
||||
{
|
||||
private readonly ElfFeatureExtractor _extractor = new();
|
||||
|
||||
[Fact]
|
||||
public void CanExtract_WithElfMagic_ReturnsTrue()
|
||||
{
|
||||
// Arrange: ELF magic bytes
|
||||
var elfBytes = new byte[] { 0x7F, 0x45, 0x4C, 0x46, 0x02, 0x01, 0x01, 0x00 };
|
||||
using var stream = new MemoryStream(elfBytes);
|
||||
|
||||
// Act
|
||||
var result = _extractor.CanExtract(stream);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanExtract_WithNonElfMagic_ReturnsFalse()
|
||||
{
|
||||
// Arrange: Not ELF
|
||||
var notElf = new byte[] { 0x4D, 0x5A, 0x90, 0x00 }; // PE magic
|
||||
using var stream = new MemoryStream(notElf);
|
||||
|
||||
// Act
|
||||
var result = _extractor.CanExtract(stream);
|
||||
|
||||
// Assert
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanExtract_WithEmptyStream_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
using var stream = new MemoryStream();
|
||||
|
||||
// Act
|
||||
var result = _extractor.CanExtract(stream);
|
||||
|
||||
// Assert
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExtractMetadataAsync_WithValidElf64_ReturnsCorrectMetadata()
|
||||
{
|
||||
// Arrange: Minimal ELF64 header (little-endian, x86_64, executable)
|
||||
var elfHeader = CreateMinimalElf64Header(
|
||||
machine: 0x3E, // x86_64
|
||||
type: 0x02, // ET_EXEC
|
||||
osabi: 0x03); // Linux
|
||||
|
||||
using var stream = new MemoryStream(elfHeader);
|
||||
|
||||
// Act
|
||||
var metadata = await _extractor.ExtractMetadataAsync(stream);
|
||||
|
||||
// Assert
|
||||
metadata.Format.Should().Be(BinaryFormat.Elf);
|
||||
metadata.Architecture.Should().Be("x86_64");
|
||||
metadata.Type.Should().Be(BinaryType.Executable);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExtractMetadataAsync_WithElf64SharedLib_ReturnsSharedLibrary()
|
||||
{
|
||||
// Arrange: ELF64 shared library
|
||||
var elfHeader = CreateMinimalElf64Header(
|
||||
machine: 0x3E,
|
||||
type: 0x03, // ET_DYN (shared object)
|
||||
osabi: 0x03);
|
||||
|
||||
using var stream = new MemoryStream(elfHeader);
|
||||
|
||||
// Act
|
||||
var metadata = await _extractor.ExtractMetadataAsync(stream);
|
||||
|
||||
// Assert
|
||||
metadata.Type.Should().Be(BinaryType.SharedLibrary);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExtractMetadataAsync_WithAarch64_ReturnsCorrectArchitecture()
|
||||
{
|
||||
// Arrange: ELF64 aarch64
|
||||
var elfHeader = CreateMinimalElf64Header(
|
||||
machine: 0xB7, // aarch64
|
||||
type: 0x02,
|
||||
osabi: 0x03);
|
||||
|
||||
using var stream = new MemoryStream(elfHeader);
|
||||
|
||||
// Act
|
||||
var metadata = await _extractor.ExtractMetadataAsync(stream);
|
||||
|
||||
// Assert
|
||||
metadata.Architecture.Should().Be("aarch64");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExtractIdentityAsync_ProducesConsistentBinaryKey()
|
||||
{
|
||||
// Arrange: Same ELF content
|
||||
var elfHeader = CreateMinimalElf64Header(machine: 0x3E, type: 0x02, osabi: 0x03);
|
||||
|
||||
using var stream1 = new MemoryStream(elfHeader);
|
||||
using var stream2 = new MemoryStream(elfHeader);
|
||||
|
||||
// Act
|
||||
var identity1 = await _extractor.ExtractIdentityAsync(stream1);
|
||||
var identity2 = await _extractor.ExtractIdentityAsync(stream2);
|
||||
|
||||
// Assert: Same content should produce same identity
|
||||
identity1.BinaryKey.Should().Be(identity2.BinaryKey);
|
||||
identity1.FileSha256.Should().Be(identity2.FileSha256);
|
||||
}
|
||||
|
||||
private static byte[] CreateMinimalElf64Header(ushort machine, ushort type, byte osabi)
|
||||
{
|
||||
var header = new byte[64];
|
||||
|
||||
// ELF magic
|
||||
header[0] = 0x7F;
|
||||
header[1] = 0x45; // E
|
||||
header[2] = 0x4C; // L
|
||||
header[3] = 0x46; // F
|
||||
|
||||
// Class: 64-bit
|
||||
header[4] = 0x02;
|
||||
// Data: little-endian
|
||||
header[5] = 0x01;
|
||||
// Version
|
||||
header[6] = 0x01;
|
||||
// OS/ABI
|
||||
header[7] = osabi;
|
||||
|
||||
// Type (little-endian)
|
||||
BitConverter.GetBytes(type).CopyTo(header, 16);
|
||||
// Machine (little-endian)
|
||||
BitConverter.GetBytes(machine).CopyTo(header, 18);
|
||||
|
||||
return header;
|
||||
}
|
||||
}
|
||||
|
||||
public class PeFeatureExtractorTests
|
||||
{
|
||||
private readonly PeFeatureExtractor _extractor = new();
|
||||
|
||||
[Fact]
|
||||
public void CanExtract_WithDosMagic_ReturnsTrue()
|
||||
{
|
||||
// Arrange: DOS/PE magic bytes
|
||||
var peBytes = CreateMinimalPeHeader();
|
||||
using var stream = new MemoryStream(peBytes);
|
||||
|
||||
// Act
|
||||
var result = _extractor.CanExtract(stream);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanExtract_WithElfMagic_ReturnsFalse()
|
||||
{
|
||||
// Arrange: ELF magic
|
||||
var elfBytes = new byte[] { 0x7F, 0x45, 0x4C, 0x46, 0x02, 0x01, 0x01, 0x00 };
|
||||
using var stream = new MemoryStream(elfBytes);
|
||||
|
||||
// Act
|
||||
var result = _extractor.CanExtract(stream);
|
||||
|
||||
// Assert
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExtractMetadataAsync_WithPe64_ReturnsCorrectMetadata()
|
||||
{
|
||||
// Arrange: PE32+ x86_64 executable
|
||||
var peHeader = CreateMinimalPeHeader(machine: 0x8664, characteristics: 0x0002);
|
||||
using var stream = new MemoryStream(peHeader);
|
||||
|
||||
// Act
|
||||
var metadata = await _extractor.ExtractMetadataAsync(stream);
|
||||
|
||||
// Assert
|
||||
metadata.Format.Should().Be(BinaryFormat.Pe);
|
||||
metadata.Architecture.Should().Be("x86_64");
|
||||
metadata.Type.Should().Be(BinaryType.Executable);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExtractMetadataAsync_WithDll_ReturnsSharedLibrary()
|
||||
{
|
||||
// Arrange: PE DLL
|
||||
var peHeader = CreateMinimalPeHeader(
|
||||
machine: 0x8664,
|
||||
characteristics: 0x2002); // IMAGE_FILE_DLL | IMAGE_FILE_EXECUTABLE_IMAGE
|
||||
|
||||
using var stream = new MemoryStream(peHeader);
|
||||
|
||||
// Act
|
||||
var metadata = await _extractor.ExtractMetadataAsync(stream);
|
||||
|
||||
// Assert
|
||||
metadata.Type.Should().Be(BinaryType.SharedLibrary);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExtractMetadataAsync_WithX86_ReturnsCorrectArchitecture()
|
||||
{
|
||||
// Arrange: PE32 x86
|
||||
var peHeader = CreateMinimalPeHeader(machine: 0x014C, characteristics: 0x0002);
|
||||
using var stream = new MemoryStream(peHeader);
|
||||
|
||||
// Act
|
||||
var metadata = await _extractor.ExtractMetadataAsync(stream);
|
||||
|
||||
// Assert
|
||||
metadata.Architecture.Should().Be("x86");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExtractIdentityAsync_ProducesConsistentBinaryKey()
|
||||
{
|
||||
// Arrange: Same PE content
|
||||
var peHeader = CreateMinimalPeHeader(machine: 0x8664, characteristics: 0x0002);
|
||||
|
||||
using var stream1 = new MemoryStream(peHeader);
|
||||
using var stream2 = new MemoryStream(peHeader);
|
||||
|
||||
// Act
|
||||
var identity1 = await _extractor.ExtractIdentityAsync(stream1);
|
||||
var identity2 = await _extractor.ExtractIdentityAsync(stream2);
|
||||
|
||||
// Assert: Same content should produce same identity
|
||||
identity1.BinaryKey.Should().Be(identity2.BinaryKey);
|
||||
identity1.FileSha256.Should().Be(identity2.FileSha256);
|
||||
}
|
||||
|
||||
private static byte[] CreateMinimalPeHeader(ushort machine = 0x8664, ushort characteristics = 0x0002)
|
||||
{
|
||||
var header = new byte[512];
|
||||
|
||||
// DOS header
|
||||
header[0] = 0x4D; // M
|
||||
header[1] = 0x5A; // Z
|
||||
|
||||
// e_lfanew at offset 0x3C
|
||||
BitConverter.GetBytes(0x80).CopyTo(header, 0x3C);
|
||||
|
||||
// PE signature at offset 0x80
|
||||
header[0x80] = 0x50; // P
|
||||
header[0x81] = 0x45; // E
|
||||
header[0x82] = 0x00;
|
||||
header[0x83] = 0x00;
|
||||
|
||||
// COFF header at 0x84
|
||||
BitConverter.GetBytes(machine).CopyTo(header, 0x84); // Machine
|
||||
BitConverter.GetBytes((ushort)0).CopyTo(header, 0x86); // NumberOfSections
|
||||
BitConverter.GetBytes((uint)0).CopyTo(header, 0x88); // TimeDateStamp
|
||||
BitConverter.GetBytes((uint)0).CopyTo(header, 0x8C); // PointerToSymbolTable
|
||||
BitConverter.GetBytes((uint)0).CopyTo(header, 0x90); // NumberOfSymbols
|
||||
BitConverter.GetBytes((ushort)240).CopyTo(header, 0x94); // SizeOfOptionalHeader (PE32+)
|
||||
BitConverter.GetBytes(characteristics).CopyTo(header, 0x96); // Characteristics
|
||||
|
||||
// Optional header magic at 0x98
|
||||
BitConverter.GetBytes((ushort)0x20B).CopyTo(header, 0x98); // PE32+ magic
|
||||
|
||||
return header;
|
||||
}
|
||||
}
|
||||
|
||||
public class MachoFeatureExtractorTests
|
||||
{
|
||||
private readonly MachoFeatureExtractor _extractor = new();
|
||||
|
||||
[Fact]
|
||||
public void CanExtract_WithMacho64Magic_ReturnsTrue()
|
||||
{
|
||||
// Arrange: Mach-O 64-bit magic
|
||||
var machoBytes = new byte[] { 0xCF, 0xFA, 0xED, 0xFE }; // MH_MAGIC_64 little-endian
|
||||
using var stream = new MemoryStream(machoBytes);
|
||||
|
||||
// Act
|
||||
var result = _extractor.CanExtract(stream);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanExtract_WithFatBinaryMagic_ReturnsTrue()
|
||||
{
|
||||
// Arrange: Universal binary magic
|
||||
var fatBytes = new byte[] { 0xCA, 0xFE, 0xBA, 0xBE }; // FAT_MAGIC
|
||||
using var stream = new MemoryStream(fatBytes);
|
||||
|
||||
// Act
|
||||
var result = _extractor.CanExtract(stream);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanExtract_WithElfMagic_ReturnsFalse()
|
||||
{
|
||||
// Arrange: ELF magic
|
||||
var elfBytes = new byte[] { 0x7F, 0x45, 0x4C, 0x46, 0x02, 0x01, 0x01, 0x00 };
|
||||
using var stream = new MemoryStream(elfBytes);
|
||||
|
||||
// Act
|
||||
var result = _extractor.CanExtract(stream);
|
||||
|
||||
// Assert
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExtractMetadataAsync_WithMacho64Executable_ReturnsCorrectMetadata()
|
||||
{
|
||||
// Arrange: Mach-O 64-bit x86_64 executable
|
||||
var machoHeader = CreateMinimalMacho64Header(
|
||||
cpuType: 0x01000007, // CPU_TYPE_X86_64
|
||||
fileType: 0x02); // MH_EXECUTE
|
||||
|
||||
using var stream = new MemoryStream(machoHeader);
|
||||
|
||||
// Act
|
||||
var metadata = await _extractor.ExtractMetadataAsync(stream);
|
||||
|
||||
// Assert
|
||||
metadata.Format.Should().Be(BinaryFormat.Macho);
|
||||
metadata.Architecture.Should().Be("x86_64");
|
||||
metadata.Type.Should().Be(BinaryType.Executable);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExtractMetadataAsync_WithDylib_ReturnsSharedLibrary()
|
||||
{
|
||||
// Arrange: Mach-O dylib
|
||||
var machoHeader = CreateMinimalMacho64Header(
|
||||
cpuType: 0x01000007,
|
||||
fileType: 0x06); // MH_DYLIB
|
||||
|
||||
using var stream = new MemoryStream(machoHeader);
|
||||
|
||||
// Act
|
||||
var metadata = await _extractor.ExtractMetadataAsync(stream);
|
||||
|
||||
// Assert
|
||||
metadata.Type.Should().Be(BinaryType.SharedLibrary);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExtractMetadataAsync_WithArm64_ReturnsCorrectArchitecture()
|
||||
{
|
||||
// Arrange: Mach-O arm64
|
||||
var machoHeader = CreateMinimalMacho64Header(
|
||||
cpuType: 0x0100000C, // CPU_TYPE_ARM64
|
||||
fileType: 0x02);
|
||||
|
||||
using var stream = new MemoryStream(machoHeader);
|
||||
|
||||
// Act
|
||||
var metadata = await _extractor.ExtractMetadataAsync(stream);
|
||||
|
||||
// Assert
|
||||
metadata.Architecture.Should().Be("aarch64");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExtractIdentityAsync_ProducesConsistentBinaryKey()
|
||||
{
|
||||
// Arrange: Same Mach-O content
|
||||
var machoHeader = CreateMinimalMacho64Header(cpuType: 0x01000007, fileType: 0x02);
|
||||
|
||||
using var stream1 = new MemoryStream(machoHeader);
|
||||
using var stream2 = new MemoryStream(machoHeader);
|
||||
|
||||
// Act
|
||||
var identity1 = await _extractor.ExtractIdentityAsync(stream1);
|
||||
var identity2 = await _extractor.ExtractIdentityAsync(stream2);
|
||||
|
||||
// Assert: Same content should produce same identity
|
||||
identity1.BinaryKey.Should().Be(identity2.BinaryKey);
|
||||
identity1.FileSha256.Should().Be(identity2.FileSha256);
|
||||
}
|
||||
|
||||
private static byte[] CreateMinimalMacho64Header(int cpuType, uint fileType)
|
||||
{
|
||||
var header = new byte[32 + 256]; // Mach-O 64 header + space for load commands
|
||||
|
||||
// Magic (little-endian)
|
||||
header[0] = 0xCF;
|
||||
header[1] = 0xFA;
|
||||
header[2] = 0xED;
|
||||
header[3] = 0xFE;
|
||||
|
||||
// CPU type
|
||||
BitConverter.GetBytes(cpuType).CopyTo(header, 4);
|
||||
// CPU subtype
|
||||
BitConverter.GetBytes(0).CopyTo(header, 8);
|
||||
// File type
|
||||
BitConverter.GetBytes(fileType).CopyTo(header, 12);
|
||||
// Number of load commands
|
||||
BitConverter.GetBytes((uint)0).CopyTo(header, 16);
|
||||
// Size of load commands
|
||||
BitConverter.GetBytes((uint)0).CopyTo(header, 20);
|
||||
// Flags
|
||||
BitConverter.GetBytes((uint)0).CopyTo(header, 24);
|
||||
// Reserved (64-bit only)
|
||||
BitConverter.GetBytes((uint)0).CopyTo(header, 28);
|
||||
|
||||
return header;
|
||||
}
|
||||
}
|
||||
|
||||
public class BinaryIdentityDeterminismTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task AllExtractors_SameContent_ProduceSameHash()
|
||||
{
|
||||
// Arrange: Create identical binary content
|
||||
var content = new byte[256];
|
||||
new Random(42).NextBytes(content);
|
||||
|
||||
// ELF header
|
||||
content[0] = 0x7F;
|
||||
content[1] = 0x45;
|
||||
content[2] = 0x4C;
|
||||
content[3] = 0x46;
|
||||
content[4] = 0x02; // 64-bit
|
||||
content[5] = 0x01; // little-endian
|
||||
BitConverter.GetBytes((ushort)0x3E).CopyTo(content, 18); // x86_64
|
||||
BitConverter.GetBytes((ushort)0x02).CopyTo(content, 16); // executable
|
||||
|
||||
var extractor = new ElfFeatureExtractor();
|
||||
|
||||
// Act: Extract identity multiple times
|
||||
using var stream1 = new MemoryStream(content);
|
||||
using var stream2 = new MemoryStream(content);
|
||||
using var stream3 = new MemoryStream(content);
|
||||
|
||||
var identity1 = await extractor.ExtractIdentityAsync(stream1);
|
||||
var identity2 = await extractor.ExtractIdentityAsync(stream2);
|
||||
var identity3 = await extractor.ExtractIdentityAsync(stream3);
|
||||
|
||||
// Assert: All identities should be identical
|
||||
identity1.FileSha256.Should().Be(identity2.FileSha256);
|
||||
identity2.FileSha256.Should().Be(identity3.FileSha256);
|
||||
identity1.BinaryKey.Should().Be(identity2.BinaryKey);
|
||||
identity2.BinaryKey.Should().Be(identity3.BinaryKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DifferentContent_ProducesDifferentHash()
|
||||
{
|
||||
// Arrange
|
||||
var content1 = CreateMinimalElf(0x01);
|
||||
var content2 = CreateMinimalElf(0x02);
|
||||
|
||||
var extractor = new ElfFeatureExtractor();
|
||||
|
||||
// Act
|
||||
using var stream1 = new MemoryStream(content1);
|
||||
using var stream2 = new MemoryStream(content2);
|
||||
|
||||
var identity1 = await extractor.ExtractIdentityAsync(stream1);
|
||||
var identity2 = await extractor.ExtractIdentityAsync(stream2);
|
||||
|
||||
// Assert: Different content should produce different identities
|
||||
identity1.FileSha256.Should().NotBe(identity2.FileSha256);
|
||||
}
|
||||
|
||||
private static byte[] CreateMinimalElf(byte variant)
|
||||
{
|
||||
var header = new byte[64];
|
||||
header[0] = 0x7F;
|
||||
header[1] = 0x45;
|
||||
header[2] = 0x4C;
|
||||
header[3] = 0x46;
|
||||
header[4] = 0x02;
|
||||
header[5] = 0x01;
|
||||
header[6] = variant; // Vary the version byte
|
||||
BitConverter.GetBytes((ushort)0x3E).CopyTo(header, 18);
|
||||
BitConverter.GetBytes((ushort)0x02).CopyTo(header, 16);
|
||||
return header;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// ParserTests.cs
|
||||
// Sprint: SPRINT_20251226_012_BINIDX_backport_handling
|
||||
// Task: BACKPORT-19 — Unit tests for all parsers
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using FluentAssertions;
|
||||
using StellaOps.BinaryIndex.FixIndex.Models;
|
||||
using StellaOps.BinaryIndex.FixIndex.Parsers;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.BinaryIndex.Core.Tests.FixIndex;
|
||||
|
||||
public class DebianChangelogParserTests
|
||||
{
|
||||
private readonly DebianChangelogParser _sut = new();
|
||||
|
||||
[Fact]
|
||||
public void ParseTopEntry_ExtractsCveFromChangelog()
|
||||
{
|
||||
// Arrange
|
||||
var changelog = """
|
||||
openssl (3.0.11-1~deb12u2) bookworm-security; urgency=high
|
||||
|
||||
* Fix CVE-2024-0727: PKCS12 decoding crash
|
||||
* Fix CVE-2024-2511: memory leak in TLSv1.3
|
||||
|
||||
-- Debian Security Team <security@debian.org> Mon, 15 Jan 2024 10:00:00 +0000
|
||||
|
||||
openssl (3.0.11-1~deb12u1) bookworm; urgency=medium
|
||||
|
||||
* Update to 3.0.11
|
||||
""";
|
||||
|
||||
// Act
|
||||
var results = _sut.ParseTopEntry(changelog, "debian", "bookworm", "openssl").ToList();
|
||||
|
||||
// Assert
|
||||
results.Should().HaveCount(2);
|
||||
results.Should().Contain(e => e.CveId == "CVE-2024-0727");
|
||||
results.Should().Contain(e => e.CveId == "CVE-2024-2511");
|
||||
results.Should().AllSatisfy(e =>
|
||||
{
|
||||
e.Distro.Should().Be("debian");
|
||||
e.Release.Should().Be("bookworm");
|
||||
e.SourcePkg.Should().Be("openssl");
|
||||
e.State.Should().Be(FixState.Fixed);
|
||||
e.FixedVersion.Should().Be("3.0.11-1~deb12u2");
|
||||
e.Method.Should().Be(FixMethod.Changelog);
|
||||
e.Confidence.Should().Be(0.80m);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseTopEntry_ReturnsEmptyForNoMention()
|
||||
{
|
||||
// Arrange
|
||||
var changelog = """
|
||||
package (1.0-1) stable; urgency=low
|
||||
|
||||
* Initial release
|
||||
|
||||
-- Maintainer <m@example.com> Mon, 01 Jan 2024 12:00:00 +0000
|
||||
""";
|
||||
|
||||
// Act
|
||||
var results = _sut.ParseTopEntry(changelog, "debian", "stable", "package").ToList();
|
||||
|
||||
// Assert
|
||||
results.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseTopEntry_HandlesEmptyChangelog()
|
||||
{
|
||||
// Act
|
||||
var results = _sut.ParseTopEntry("", "debian", "stable", "package").ToList();
|
||||
|
||||
// Assert
|
||||
results.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseTopEntry_DeduplicatesCves()
|
||||
{
|
||||
// Arrange - Same CVE mentioned twice
|
||||
var changelog = """
|
||||
package (1.0-1) stable; urgency=high
|
||||
|
||||
* Fix CVE-2024-1234 in parser
|
||||
* Also addresses CVE-2024-1234 in handler
|
||||
|
||||
-- Maintainer <m@example.com> Mon, 01 Jan 2024 12:00:00 +0000
|
||||
""";
|
||||
|
||||
// Act
|
||||
var results = _sut.ParseTopEntry(changelog, "debian", "stable", "package").ToList();
|
||||
|
||||
// Assert
|
||||
results.Should().HaveCount(1);
|
||||
results[0].CveId.Should().Be("CVE-2024-1234");
|
||||
}
|
||||
}
|
||||
|
||||
public class AlpineSecfixesParserTests
|
||||
{
|
||||
private readonly AlpineSecfixesParser _sut = new();
|
||||
|
||||
[Fact]
|
||||
public void Parse_ExtractsCvesFromSecfixes()
|
||||
{
|
||||
// Arrange
|
||||
var apkbuild = """
|
||||
pkgname=openssl
|
||||
pkgver=3.1.4
|
||||
pkgrel=1
|
||||
|
||||
# secfixes:
|
||||
# 3.1.4-r0:
|
||||
# - CVE-2024-0727
|
||||
# - CVE-2024-2511
|
||||
# 3.1.3-r0:
|
||||
# - CVE-2023-5678
|
||||
|
||||
build() {
|
||||
./configure
|
||||
}
|
||||
""";
|
||||
|
||||
// Act
|
||||
var results = _sut.Parse(apkbuild, "alpine", "v3.19", "openssl").ToList();
|
||||
|
||||
// Assert
|
||||
results.Should().HaveCount(3);
|
||||
|
||||
var v314 = results.Where(e => e.FixedVersion == "3.1.4-r0").ToList();
|
||||
v314.Should().HaveCount(2);
|
||||
v314.Should().Contain(e => e.CveId == "CVE-2024-0727");
|
||||
v314.Should().Contain(e => e.CveId == "CVE-2024-2511");
|
||||
|
||||
var v313 = results.Where(e => e.FixedVersion == "3.1.3-r0").ToList();
|
||||
v313.Should().HaveCount(1);
|
||||
v313[0].CveId.Should().Be("CVE-2023-5678");
|
||||
|
||||
results.Should().AllSatisfy(e =>
|
||||
{
|
||||
e.Distro.Should().Be("alpine");
|
||||
e.Release.Should().Be("v3.19");
|
||||
e.State.Should().Be(FixState.Fixed);
|
||||
e.Method.Should().Be(FixMethod.SecurityFeed);
|
||||
e.Confidence.Should().Be(0.95m);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_IgnoresNonSecfixesComments()
|
||||
{
|
||||
// Arrange
|
||||
var apkbuild = """
|
||||
# This is a regular comment
|
||||
# CVE-2024-9999 is not in secfixes
|
||||
pkgname=test
|
||||
""";
|
||||
|
||||
// Act
|
||||
var results = _sut.Parse(apkbuild, "alpine", "v3.19", "test").ToList();
|
||||
|
||||
// Assert
|
||||
results.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_StopsAtNonCommentLine()
|
||||
{
|
||||
// Arrange
|
||||
var apkbuild = """
|
||||
# secfixes:
|
||||
# 1.0-r0:
|
||||
# - CVE-2024-1111
|
||||
pkgname=test
|
||||
# - CVE-2024-2222
|
||||
""";
|
||||
|
||||
// Act
|
||||
var results = _sut.Parse(apkbuild, "alpine", "edge", "test").ToList();
|
||||
|
||||
// Assert
|
||||
results.Should().HaveCount(1);
|
||||
results[0].CveId.Should().Be("CVE-2024-1111");
|
||||
}
|
||||
}
|
||||
|
||||
public class PatchHeaderParserTests
|
||||
{
|
||||
private readonly PatchHeaderParser _sut = new();
|
||||
|
||||
[Fact]
|
||||
public void ParsePatches_ExtractsCveFromHeader()
|
||||
{
|
||||
// Arrange
|
||||
var patches = new[]
|
||||
{
|
||||
(
|
||||
Path: "debian/patches/CVE-2024-1234.patch",
|
||||
Content: """
|
||||
Description: Fix buffer overflow
|
||||
Origin: upstream, https://github.com/proj/commit/abc123
|
||||
Bug-Debian: https://bugs.debian.org/123456
|
||||
CVE: CVE-2024-1234
|
||||
Applied-Upstream: 2.0.0
|
||||
|
||||
--- a/src/parser.c
|
||||
+++ b/src/parser.c
|
||||
@@ -100,6 +100,8 @@
|
||||
""",
|
||||
Sha256: "abc123def456"
|
||||
)
|
||||
};
|
||||
|
||||
// Act
|
||||
var results = _sut.ParsePatches(patches, "debian", "bookworm", "libfoo", "1.2.3-1").ToList();
|
||||
|
||||
// Assert
|
||||
results.Should().HaveCount(1);
|
||||
results[0].CveId.Should().Be("CVE-2024-1234");
|
||||
results[0].Method.Should().Be(FixMethod.PatchHeader);
|
||||
results[0].FixedVersion.Should().Be("1.2.3-1");
|
||||
results[0].Evidence.Should().BeOfType<PatchHeaderEvidence>();
|
||||
|
||||
var evidence = (PatchHeaderEvidence)results[0].Evidence;
|
||||
evidence.PatchPath.Should().Be("debian/patches/CVE-2024-1234.patch");
|
||||
evidence.PatchSha256.Should().Be("abc123def456");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsePatches_ExtractsCveFromFilename()
|
||||
{
|
||||
// Arrange - CVE only in filename, not header
|
||||
var patches = new[]
|
||||
{
|
||||
(
|
||||
Path: "CVE-2024-5678.patch",
|
||||
Content: """
|
||||
Fix memory leak
|
||||
|
||||
--- a/foo.c
|
||||
+++ b/foo.c
|
||||
""",
|
||||
Sha256: "sha256hash"
|
||||
)
|
||||
};
|
||||
|
||||
// Act
|
||||
var results = _sut.ParsePatches(patches, "ubuntu", "jammy", "bar", "1.0").ToList();
|
||||
|
||||
// Assert
|
||||
results.Should().HaveCount(1);
|
||||
results[0].CveId.Should().Be("CVE-2024-5678");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsePatches_ReturnsEmptyForNoCve()
|
||||
{
|
||||
// Arrange
|
||||
var patches = new[]
|
||||
{
|
||||
(
|
||||
Path: "fix-typo.patch",
|
||||
Content: "--- a/README\n+++ b/README",
|
||||
Sha256: "hash"
|
||||
)
|
||||
};
|
||||
|
||||
// Act
|
||||
var results = _sut.ParsePatches(patches, "debian", "sid", "pkg", "1.0").ToList();
|
||||
|
||||
// Assert
|
||||
results.Should().BeEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public class RpmChangelogParserTests
|
||||
{
|
||||
private readonly RpmChangelogParser _sut = new();
|
||||
|
||||
[Fact]
|
||||
public void ParseTopEntry_ExtractsCveFromSpecChangelog()
|
||||
{
|
||||
// Arrange
|
||||
var spec = """
|
||||
Name: openssl
|
||||
Version: 3.0.7
|
||||
Release: 27.el9
|
||||
|
||||
%description
|
||||
OpenSSL toolkit
|
||||
|
||||
%changelog
|
||||
* Mon Jan 15 2024 Security Team <security@redhat.com> - 3.0.7-27
|
||||
- Fix CVE-2024-0727: PKCS12 crash
|
||||
- Fix CVE-2024-2511: memory leak
|
||||
|
||||
* Tue Dec 05 2023 Security Team <security@redhat.com> - 3.0.7-26
|
||||
- Fix CVE-2023-5678
|
||||
""";
|
||||
|
||||
// Act
|
||||
var results = _sut.ParseTopEntry(spec, "rhel", "9", "openssl").ToList();
|
||||
|
||||
// Assert
|
||||
results.Should().HaveCount(2);
|
||||
results.Should().Contain(e => e.CveId == "CVE-2024-0727");
|
||||
results.Should().Contain(e => e.CveId == "CVE-2024-2511");
|
||||
results.Should().AllSatisfy(e =>
|
||||
{
|
||||
e.Distro.Should().Be("rhel");
|
||||
e.Release.Should().Be("9");
|
||||
e.FixedVersion.Should().Be("3.0.7-27");
|
||||
e.Method.Should().Be(FixMethod.Changelog);
|
||||
e.Confidence.Should().Be(0.75m);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseAllEntries_ExtractsFromMultipleEntries()
|
||||
{
|
||||
// Arrange
|
||||
var spec = """
|
||||
%changelog
|
||||
* Mon Jan 15 2024 Packager <p@example.com> - 2.0-1
|
||||
- Fix CVE-2024-1111
|
||||
|
||||
* Mon Dec 01 2023 Packager <p@example.com> - 1.9-1
|
||||
- Fix CVE-2023-2222
|
||||
- Fix CVE-2023-3333
|
||||
""";
|
||||
|
||||
// Act
|
||||
var results = _sut.ParseAllEntries(spec, "fedora", "39", "pkg").ToList();
|
||||
|
||||
// Assert
|
||||
results.Should().HaveCount(3);
|
||||
|
||||
var v20 = results.Where(e => e.FixedVersion == "2.0-1").ToList();
|
||||
v20.Should().HaveCount(1);
|
||||
v20[0].CveId.Should().Be("CVE-2024-1111");
|
||||
|
||||
var v19 = results.Where(e => e.FixedVersion == "1.9-1").ToList();
|
||||
v19.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseTopEntry_StopsAtSecondEntry()
|
||||
{
|
||||
// Arrange
|
||||
var spec = """
|
||||
%changelog
|
||||
* Mon Jan 15 2024 P <p@x.com> - 2.0-1
|
||||
- Fix CVE-2024-1111
|
||||
|
||||
* Mon Dec 01 2023 P <p@x.com> - 1.9-1
|
||||
- Fix CVE-2023-2222
|
||||
""";
|
||||
|
||||
// Act
|
||||
var results = _sut.ParseTopEntry(spec, "centos", "9", "pkg").ToList();
|
||||
|
||||
// Assert
|
||||
results.Should().HaveCount(1);
|
||||
results[0].CveId.Should().Be("CVE-2024-1111");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseTopEntry_HandlesNoChangelog()
|
||||
{
|
||||
// Arrange
|
||||
var spec = """
|
||||
Name: test
|
||||
Version: 1.0
|
||||
""";
|
||||
|
||||
// Act
|
||||
var results = _sut.ParseTopEntry(spec, "rhel", "9", "test").ToList();
|
||||
|
||||
// Assert
|
||||
results.Should().BeEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.BinaryIndex.Core\StellaOps.BinaryIndex.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user