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:
StellaOps Bot
2025-12-26 15:17:15 +02:00
parent 7792749bb4
commit 907783f625
354 changed files with 79727 additions and 1346 deletions

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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>