partly or unimplemented features - now implemented
This commit is contained in:
@@ -35,6 +35,9 @@ public static class GoldenSetServiceCollectionExtensions
|
||||
services.TryAddSingleton<ISinkRegistry, SinkRegistry>();
|
||||
services.TryAddSingleton<IGoldenSetValidator, GoldenSetValidator>();
|
||||
|
||||
// Cross-distro coverage matrix for backport validation
|
||||
services.TryAddSingleton<ICrossDistroCoverageService, CrossDistroCoverageService>();
|
||||
|
||||
// Memory cache (if not already registered)
|
||||
services.AddMemoryCache();
|
||||
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// CrossDistroCoverageModels.cs
|
||||
// Sprint: SPRINT_20260208_027_BinaryIndex_cross_distro_golden_set_for_backport_validation
|
||||
// Task: T1 — Cross-distro coverage matrix models for backport validation
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.BinaryIndex.GoldenSet;
|
||||
|
||||
/// <summary>
|
||||
/// Supported Linux distributions for cross-distro backport validation.
|
||||
/// </summary>
|
||||
public enum DistroFamily
|
||||
{
|
||||
/// <summary>Alpine Linux (musl libc, APK).</summary>
|
||||
Alpine = 0,
|
||||
|
||||
/// <summary>Debian / Ubuntu (glibc, DEB).</summary>
|
||||
Debian = 1,
|
||||
|
||||
/// <summary>RHEL / CentOS / Fedora (glibc, RPM).</summary>
|
||||
Rhel = 2
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Backport status for a given CVE on a specific distribution version.
|
||||
/// </summary>
|
||||
public enum BackportStatus
|
||||
{
|
||||
/// <summary>Fix has not been applied (still vulnerable).</summary>
|
||||
NotPatched = 0,
|
||||
|
||||
/// <summary>Fix has been backported to the package version.</summary>
|
||||
Backported = 1,
|
||||
|
||||
/// <summary>The component was removed or is not applicable.</summary>
|
||||
NotApplicable = 2,
|
||||
|
||||
/// <summary>Backport status is unknown or not yet validated.</summary>
|
||||
Unknown = 3
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A single distro-version coverage entry in the cross-distro matrix.
|
||||
/// Tracks whether a given CVE's fix has been backported to a specific distro version.
|
||||
/// </summary>
|
||||
public sealed record DistroCoverageEntry
|
||||
{
|
||||
/// <summary>Distribution family (Alpine, Debian, RHEL).</summary>
|
||||
public required DistroFamily Distro { get; init; }
|
||||
|
||||
/// <summary>Distro release version (e.g., "3.18", "bookworm", "9").</summary>
|
||||
public required string Version { get; init; }
|
||||
|
||||
/// <summary>Package name in the distro's packaging system.</summary>
|
||||
public required string PackageName { get; init; }
|
||||
|
||||
/// <summary>Package version string in the distro's format.</summary>
|
||||
public required string PackageVersion { get; init; }
|
||||
|
||||
/// <summary>Backport status for this entry.</summary>
|
||||
public required BackportStatus Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the golden set definition has been validated against
|
||||
/// a real binary from this distro version.
|
||||
/// </summary>
|
||||
public bool Validated { get; init; }
|
||||
|
||||
/// <summary>When this entry was last validated (null if never).</summary>
|
||||
public DateTimeOffset? ValidatedAt { get; init; }
|
||||
|
||||
/// <summary>Optional notes (e.g., patch commit hash, advisory URL).</summary>
|
||||
public string? Notes { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A curated high-impact CVE entry with cross-distro coverage information.
|
||||
/// Represents one row in the "golden set" of curated cross-distro test cases.
|
||||
/// </summary>
|
||||
public sealed record CuratedCveEntry
|
||||
{
|
||||
/// <summary>CVE identifier (e.g., "CVE-2014-0160").</summary>
|
||||
public required string CveId { get; init; }
|
||||
|
||||
/// <summary>Affected component (e.g., "openssl", "sudo").</summary>
|
||||
public required string Component { get; init; }
|
||||
|
||||
/// <summary>Human-readable vulnerability name (e.g., "Heartbleed").</summary>
|
||||
public string? CommonName { get; init; }
|
||||
|
||||
/// <summary>CVSS score (0.0 – 10.0).</summary>
|
||||
public double? CvssScore { get; init; }
|
||||
|
||||
/// <summary>CWE identifiers associated with this CVE.</summary>
|
||||
public ImmutableArray<string> CweIds { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Per-distro coverage entries showing backport status.
|
||||
/// Keyed by (Distro, Version) for efficient lookup.
|
||||
/// </summary>
|
||||
public required ImmutableArray<DistroCoverageEntry> Coverage { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Reference to the golden set definition for this CVE.
|
||||
/// Null if not yet linked to a validated golden set.
|
||||
/// </summary>
|
||||
public string? GoldenSetId { get; init; }
|
||||
|
||||
/// <summary>When this curated entry was created.</summary>
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
|
||||
/// <summary>When this curated entry was last updated.</summary>
|
||||
public DateTimeOffset? UpdatedAt { get; init; }
|
||||
|
||||
/// <summary>Number of distro-version entries that have been validated.</summary>
|
||||
public int ValidatedCount => Coverage.IsDefaultOrEmpty ? 0 : Coverage.Count(c => c.Validated);
|
||||
|
||||
/// <summary>Total number of distro-version entries.</summary>
|
||||
public int TotalEntries => Coverage.IsDefaultOrEmpty ? 0 : Coverage.Length;
|
||||
|
||||
/// <summary>Coverage ratio [0.0, 1.0].</summary>
|
||||
public double CoverageRatio => TotalEntries == 0 ? 0.0 : (double)ValidatedCount / TotalEntries;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aggregated coverage summary across all curated CVEs.
|
||||
/// </summary>
|
||||
public sealed record CrossDistroCoverageSummary
|
||||
{
|
||||
/// <summary>Total curated CVEs in the matrix.</summary>
|
||||
public required int TotalCves { get; init; }
|
||||
|
||||
/// <summary>Total distro-version entries across all CVEs.</summary>
|
||||
public required int TotalEntries { get; init; }
|
||||
|
||||
/// <summary>Number of validated entries.</summary>
|
||||
public required int ValidatedEntries { get; init; }
|
||||
|
||||
/// <summary>Number of entries where the fix is backported.</summary>
|
||||
public required int BackportedCount { get; init; }
|
||||
|
||||
/// <summary>Number of entries where the component is not patched.</summary>
|
||||
public required int NotPatchedCount { get; init; }
|
||||
|
||||
/// <summary>Per-distro breakdown.</summary>
|
||||
public required ImmutableDictionary<DistroFamily, DistroBreakdown> ByDistro { get; init; }
|
||||
|
||||
/// <summary>Overall validation coverage ratio [0.0, 1.0].</summary>
|
||||
public double OverallCoverage => TotalEntries == 0 ? 0.0 : (double)ValidatedEntries / TotalEntries;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Per-distro breakdown within the coverage summary.
|
||||
/// </summary>
|
||||
public sealed record DistroBreakdown
|
||||
{
|
||||
/// <summary>Number of entries for this distro family.</summary>
|
||||
public required int EntryCount { get; init; }
|
||||
|
||||
/// <summary>Number of validated entries for this distro.</summary>
|
||||
public required int ValidatedCount { get; init; }
|
||||
|
||||
/// <summary>Number of backported entries for this distro.</summary>
|
||||
public required int BackportedCount { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Query parameters for filtering curated CVE entries.
|
||||
/// </summary>
|
||||
public sealed record CuratedCveQuery
|
||||
{
|
||||
/// <summary>Filter by component name (case-insensitive substring).</summary>
|
||||
public string? Component { get; init; }
|
||||
|
||||
/// <summary>Filter by distro family.</summary>
|
||||
public DistroFamily? Distro { get; init; }
|
||||
|
||||
/// <summary>Filter by backport status.</summary>
|
||||
public BackportStatus? Status { get; init; }
|
||||
|
||||
/// <summary>Only return entries that haven't been validated yet.</summary>
|
||||
public bool OnlyUnvalidated { get; init; }
|
||||
|
||||
/// <summary>Maximum results to return.</summary>
|
||||
public int Limit { get; init; } = 100;
|
||||
|
||||
/// <summary>Offset for paging.</summary>
|
||||
public int Offset { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// CrossDistroCoverageService.cs
|
||||
// Sprint: SPRINT_20260208_027_BinaryIndex_cross_distro_golden_set_for_backport_validation
|
||||
// Task: T1 — Cross-distro coverage matrix service implementation
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.Metrics;
|
||||
|
||||
namespace StellaOps.BinaryIndex.GoldenSet;
|
||||
|
||||
/// <summary>
|
||||
/// In-memory implementation of the cross-distro coverage matrix.
|
||||
/// Manages curated CVE entries with per-distro backport validation status.
|
||||
/// </summary>
|
||||
public sealed class CrossDistroCoverageService : ICrossDistroCoverageService
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, CuratedCveEntry> _entries = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
private readonly Counter<long> _upsertCounter;
|
||||
private readonly Counter<long> _queryCounter;
|
||||
private readonly Counter<long> _seedCounter;
|
||||
private readonly Counter<long> _validatedCounter;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new cross-distro coverage service with OTel instrumentation.
|
||||
/// </summary>
|
||||
public CrossDistroCoverageService(IMeterFactory meterFactory, TimeProvider? timeProvider = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(meterFactory);
|
||||
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
|
||||
var meter = meterFactory.Create("StellaOps.BinaryIndex.GoldenSet.CrossDistro");
|
||||
_upsertCounter = meter.CreateCounter<long>("crossdistro.upsert.total", description: "CVE entries upserted");
|
||||
_queryCounter = meter.CreateCounter<long>("crossdistro.query.total", description: "Coverage queries executed");
|
||||
_seedCounter = meter.CreateCounter<long>("crossdistro.seed.total", description: "Built-in entries seeded");
|
||||
_validatedCounter = meter.CreateCounter<long>("crossdistro.validated.total", description: "Entries marked as validated");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<CuratedCveEntry> UpsertAsync(CuratedCveEntry entry, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entry);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(entry.CveId);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var updated = entry with { UpdatedAt = now };
|
||||
_entries[entry.CveId] = updated;
|
||||
|
||||
_upsertCounter.Add(1);
|
||||
return Task.FromResult(updated);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<CuratedCveEntry?> GetByCveIdAsync(string cveId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(cveId);
|
||||
|
||||
_entries.TryGetValue(cveId, out var entry);
|
||||
return Task.FromResult(entry);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<ImmutableArray<CuratedCveEntry>> QueryAsync(CuratedCveQuery query, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(query);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
_queryCounter.Add(1);
|
||||
|
||||
IEnumerable<CuratedCveEntry> results = _entries.Values;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query.Component))
|
||||
{
|
||||
results = results.Where(e =>
|
||||
e.Component.Contains(query.Component, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
if (query.Distro is { } distro)
|
||||
{
|
||||
results = results.Where(e =>
|
||||
!e.Coverage.IsDefaultOrEmpty &&
|
||||
e.Coverage.Any(c => c.Distro == distro));
|
||||
}
|
||||
|
||||
if (query.Status is { } status)
|
||||
{
|
||||
results = results.Where(e =>
|
||||
!e.Coverage.IsDefaultOrEmpty &&
|
||||
e.Coverage.Any(c => c.Status == status));
|
||||
}
|
||||
|
||||
if (query.OnlyUnvalidated)
|
||||
{
|
||||
results = results.Where(e =>
|
||||
!e.Coverage.IsDefaultOrEmpty &&
|
||||
e.Coverage.Any(c => !c.Validated));
|
||||
}
|
||||
|
||||
var ordered = results
|
||||
.OrderBy(e => e.CveId, StringComparer.OrdinalIgnoreCase)
|
||||
.Skip(query.Offset)
|
||||
.Take(query.Limit)
|
||||
.ToImmutableArray();
|
||||
|
||||
return Task.FromResult(ordered);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<CrossDistroCoverageSummary> GetSummaryAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var allEntries = _entries.Values.ToList();
|
||||
var allCoverage = allEntries
|
||||
.Where(e => !e.Coverage.IsDefaultOrEmpty)
|
||||
.SelectMany(e => e.Coverage)
|
||||
.ToList();
|
||||
|
||||
var byDistro = new Dictionary<DistroFamily, DistroBreakdown>();
|
||||
foreach (var distro in Enum.GetValues<DistroFamily>())
|
||||
{
|
||||
var distroEntries = allCoverage.Where(c => c.Distro == distro).ToList();
|
||||
byDistro[distro] = new DistroBreakdown
|
||||
{
|
||||
EntryCount = distroEntries.Count,
|
||||
ValidatedCount = distroEntries.Count(c => c.Validated),
|
||||
BackportedCount = distroEntries.Count(c => c.Status == BackportStatus.Backported)
|
||||
};
|
||||
}
|
||||
|
||||
var summary = new CrossDistroCoverageSummary
|
||||
{
|
||||
TotalCves = allEntries.Count,
|
||||
TotalEntries = allCoverage.Count,
|
||||
ValidatedEntries = allCoverage.Count(c => c.Validated),
|
||||
BackportedCount = allCoverage.Count(c => c.Status == BackportStatus.Backported),
|
||||
NotPatchedCount = allCoverage.Count(c => c.Status == BackportStatus.NotPatched),
|
||||
ByDistro = byDistro.ToImmutableDictionary()
|
||||
};
|
||||
|
||||
return Task.FromResult(summary);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<bool> SetValidatedAsync(
|
||||
string cveId,
|
||||
DistroFamily distro,
|
||||
string version,
|
||||
bool validated,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(cveId);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(version);
|
||||
|
||||
if (!_entries.TryGetValue(cveId, out var entry))
|
||||
return Task.FromResult(false);
|
||||
|
||||
if (entry.Coverage.IsDefaultOrEmpty)
|
||||
return Task.FromResult(false);
|
||||
|
||||
var index = -1;
|
||||
for (var i = 0; i < entry.Coverage.Length; i++)
|
||||
{
|
||||
var candidate = entry.Coverage[i];
|
||||
if (candidate.Distro == distro &&
|
||||
candidate.Version.Equals(version, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (index < 0)
|
||||
return Task.FromResult(false);
|
||||
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var updated = entry.Coverage[index] with
|
||||
{
|
||||
Validated = validated,
|
||||
ValidatedAt = validated ? now : null
|
||||
};
|
||||
|
||||
var newCoverage = entry.Coverage.SetItem(index, updated);
|
||||
_entries[cveId] = entry with { Coverage = newCoverage, UpdatedAt = now };
|
||||
|
||||
_validatedCounter.Add(1);
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<int> SeedBuiltInEntriesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var seeded = 0;
|
||||
|
||||
foreach (var entry in CreateBuiltInEntries(now))
|
||||
{
|
||||
if (_entries.TryAdd(entry.CveId, entry))
|
||||
seeded++;
|
||||
}
|
||||
|
||||
_seedCounter.Add(seeded);
|
||||
return Task.FromResult(seeded);
|
||||
}
|
||||
|
||||
// ── Built-in curated CVEs for cross-distro backport validation ─────
|
||||
|
||||
internal static ImmutableArray<CuratedCveEntry> CreateBuiltInEntries(DateTimeOffset createdAt)
|
||||
{
|
||||
return
|
||||
[
|
||||
// OpenSSL Heartbleed — buffer over-read in TLS heartbeat extension
|
||||
new CuratedCveEntry
|
||||
{
|
||||
CveId = "CVE-2014-0160",
|
||||
Component = "openssl",
|
||||
CommonName = "Heartbleed",
|
||||
CvssScore = 7.5,
|
||||
CweIds = ["CWE-126"],
|
||||
GoldenSetId = "CVE-2014-0160",
|
||||
CreatedAt = createdAt,
|
||||
Coverage =
|
||||
[
|
||||
Entry(DistroFamily.Alpine, "3.9", "openssl", "1.0.2k-r0", BackportStatus.NotPatched),
|
||||
Entry(DistroFamily.Alpine, "3.18", "openssl", "3.1.1-r0", BackportStatus.Backported),
|
||||
Entry(DistroFamily.Debian, "wheezy", "openssl", "1.0.1e-2+deb7u7", BackportStatus.Backported),
|
||||
Entry(DistroFamily.Debian, "bookworm", "openssl", "3.0.11-1~deb12u1", BackportStatus.Backported),
|
||||
Entry(DistroFamily.Rhel, "6", "openssl", "1.0.1e-16.el6_5.7", BackportStatus.Backported),
|
||||
Entry(DistroFamily.Rhel, "9", "openssl", "3.0.7-17.el9", BackportStatus.Backported),
|
||||
]
|
||||
},
|
||||
|
||||
// sudo Baron Samedit — heap-based buffer overflow in sudoers parsing
|
||||
new CuratedCveEntry
|
||||
{
|
||||
CveId = "CVE-2021-3156",
|
||||
Component = "sudo",
|
||||
CommonName = "Baron Samedit",
|
||||
CvssScore = 7.8,
|
||||
CweIds = ["CWE-122"],
|
||||
GoldenSetId = "CVE-2021-3156",
|
||||
CreatedAt = createdAt,
|
||||
Coverage =
|
||||
[
|
||||
Entry(DistroFamily.Alpine, "3.12", "sudo", "1.9.5p2-r0", BackportStatus.Backported),
|
||||
Entry(DistroFamily.Alpine, "3.18", "sudo", "1.9.13p3-r0", BackportStatus.Backported),
|
||||
Entry(DistroFamily.Debian, "buster", "sudo", "1.8.27-1+deb10u3", BackportStatus.Backported),
|
||||
Entry(DistroFamily.Debian, "bookworm", "sudo", "1.9.13p3-1+deb12u1", BackportStatus.Backported),
|
||||
Entry(DistroFamily.Rhel, "7", "sudo", "1.8.23-10.el7_9.3", BackportStatus.Backported),
|
||||
Entry(DistroFamily.Rhel, "9", "sudo", "1.9.5p2-9.el9", BackportStatus.Backported),
|
||||
]
|
||||
},
|
||||
|
||||
// glibc — stack buffer overflow in __nss_hostname_digits_dots
|
||||
new CuratedCveEntry
|
||||
{
|
||||
CveId = "CVE-2015-0235",
|
||||
Component = "glibc",
|
||||
CommonName = "GHOST",
|
||||
CvssScore = 10.0,
|
||||
CweIds = ["CWE-787"],
|
||||
GoldenSetId = "CVE-2015-0235",
|
||||
CreatedAt = createdAt,
|
||||
Coverage =
|
||||
[
|
||||
Entry(DistroFamily.Alpine, "3.18", "musl", "1.2.4-r0", BackportStatus.NotApplicable),
|
||||
Entry(DistroFamily.Debian, "wheezy", "eglibc", "2.13-38+deb7u8", BackportStatus.Backported),
|
||||
Entry(DistroFamily.Debian, "bookworm", "glibc", "2.36-9+deb12u3", BackportStatus.Backported),
|
||||
Entry(DistroFamily.Rhel, "6", "glibc", "2.12-1.149.el6_6.5", BackportStatus.Backported),
|
||||
Entry(DistroFamily.Rhel, "9", "glibc", "2.34-60.el9", BackportStatus.Backported),
|
||||
]
|
||||
},
|
||||
|
||||
// curl — SOCKS5 heap-based buffer overflow
|
||||
new CuratedCveEntry
|
||||
{
|
||||
CveId = "CVE-2023-38545",
|
||||
Component = "curl",
|
||||
CommonName = "SOCKS5 heap overflow",
|
||||
CvssScore = 9.8,
|
||||
CweIds = ["CWE-787"],
|
||||
GoldenSetId = "CVE-2023-38545",
|
||||
CreatedAt = createdAt,
|
||||
Coverage =
|
||||
[
|
||||
Entry(DistroFamily.Alpine, "3.18", "curl", "8.4.0-r0", BackportStatus.Backported),
|
||||
Entry(DistroFamily.Debian, "bookworm", "curl", "7.88.1-10+deb12u4", BackportStatus.Backported),
|
||||
Entry(DistroFamily.Debian, "bullseye", "curl", "7.74.0-1.3+deb11u10", BackportStatus.Backported),
|
||||
Entry(DistroFamily.Rhel, "8", "curl", "7.61.1-30.el8_8.4", BackportStatus.Backported),
|
||||
Entry(DistroFamily.Rhel, "9", "curl", "8.0.1-1.el9", BackportStatus.Backported),
|
||||
]
|
||||
},
|
||||
|
||||
// OpenSSH — regreSSHion (signal handler race condition)
|
||||
new CuratedCveEntry
|
||||
{
|
||||
CveId = "CVE-2024-6387",
|
||||
Component = "openssh",
|
||||
CommonName = "regreSSHion",
|
||||
CvssScore = 8.1,
|
||||
CweIds = ["CWE-362"],
|
||||
GoldenSetId = "CVE-2024-6387",
|
||||
CreatedAt = createdAt,
|
||||
Coverage =
|
||||
[
|
||||
Entry(DistroFamily.Alpine, "3.18", "openssh", "9.3_p2-r0", BackportStatus.Backported),
|
||||
Entry(DistroFamily.Alpine, "3.20", "openssh", "9.7_p1-r4", BackportStatus.Backported),
|
||||
Entry(DistroFamily.Debian, "bookworm", "openssh", "1:9.2p1-2+deb12u3", BackportStatus.Backported),
|
||||
Entry(DistroFamily.Rhel, "8", "openssh", "8.0p1-19.el8_8", BackportStatus.NotPatched),
|
||||
Entry(DistroFamily.Rhel, "9", "openssh", "8.7p1-38.el9_4.1", BackportStatus.Backported),
|
||||
]
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private static DistroCoverageEntry Entry(
|
||||
DistroFamily distro,
|
||||
string version,
|
||||
string package,
|
||||
string packageVersion,
|
||||
BackportStatus status) => new()
|
||||
{
|
||||
Distro = distro,
|
||||
Version = version,
|
||||
PackageName = package,
|
||||
PackageVersion = packageVersion,
|
||||
Status = status
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// ICrossDistroCoverageService.cs
|
||||
// Sprint: SPRINT_20260208_027_BinaryIndex_cross_distro_golden_set_for_backport_validation
|
||||
// Task: T1 — Interface for cross-distro coverage matrix management
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.BinaryIndex.GoldenSet;
|
||||
|
||||
/// <summary>
|
||||
/// Manages the cross-distro coverage matrix for curated CVEs,
|
||||
/// enabling backport validation across Alpine, Debian, and RHEL.
|
||||
/// </summary>
|
||||
public interface ICrossDistroCoverageService
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds or updates a curated CVE entry with its cross-distro coverage data.
|
||||
/// </summary>
|
||||
Task<CuratedCveEntry> UpsertAsync(CuratedCveEntry entry, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a curated CVE entry by its CVE ID.
|
||||
/// </summary>
|
||||
Task<CuratedCveEntry?> GetByCveIdAsync(string cveId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Queries curated CVE entries with filtering.
|
||||
/// </summary>
|
||||
Task<ImmutableArray<CuratedCveEntry>> QueryAsync(CuratedCveQuery query, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Computes a summary of cross-distro coverage across all curated CVEs.
|
||||
/// </summary>
|
||||
Task<CrossDistroCoverageSummary> GetSummaryAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Marks a specific distro coverage entry as validated (or not).
|
||||
/// </summary>
|
||||
Task<bool> SetValidatedAsync(
|
||||
string cveId,
|
||||
DistroFamily distro,
|
||||
string version,
|
||||
bool validated,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Seeds the coverage matrix with built-in high-impact CVE entries.
|
||||
/// Idempotent: only adds entries that don't already exist.
|
||||
/// </summary>
|
||||
Task<int> SeedBuiltInEntriesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,459 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// ElfSegmentNormalizer.cs
|
||||
// Sprint: SPRINT_20260208_028_BinaryIndex_elf_normalization_and_delta_hashing
|
||||
// Task: T1 — ELF segment-level normalization for position-independent delta hashing
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace StellaOps.BinaryIndex.Normalization;
|
||||
|
||||
/// <summary>
|
||||
/// Type of ELF segment normalization applied to raw binary bytes.
|
||||
/// </summary>
|
||||
public enum ElfNormalizationStep
|
||||
{
|
||||
/// <summary>Relocation table entries zeroed.</summary>
|
||||
RelocationZeroing = 0,
|
||||
|
||||
/// <summary>GOT/PLT entries replaced with canonical stubs.</summary>
|
||||
GotPltCanonicalization = 1,
|
||||
|
||||
/// <summary>NOP sleds normalized to canonical NOP bytes.</summary>
|
||||
NopCanonicalization = 2,
|
||||
|
||||
/// <summary>Jump table entries rewritten to relative offsets.</summary>
|
||||
JumpTableRewriting = 3,
|
||||
|
||||
/// <summary>Alignment padding zeroed.</summary>
|
||||
PaddingZeroing = 4
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options controlling ELF segment normalization.
|
||||
/// </summary>
|
||||
public sealed record ElfSegmentNormalizationOptions
|
||||
{
|
||||
/// <summary>Zero out relocation entries (REL/RELA sections).</summary>
|
||||
public bool ZeroRelocations { get; init; } = true;
|
||||
|
||||
/// <summary>Canonicalize GOT/PLT entries to remove position-dependent bytes.</summary>
|
||||
public bool CanonicalizeGotPlt { get; init; } = true;
|
||||
|
||||
/// <summary>Collapse x86/x64 NOP variants to canonical 0x90.</summary>
|
||||
public bool CanonicalizeNops { get; init; } = true;
|
||||
|
||||
/// <summary>Rewrite jump table entries to position-independent form.</summary>
|
||||
public bool RewriteJumpTables { get; init; } = true;
|
||||
|
||||
/// <summary>Zero out alignment padding between sections.</summary>
|
||||
public bool ZeroPadding { get; init; } = true;
|
||||
|
||||
/// <summary>Default options for maximum normalization.</summary>
|
||||
public static ElfSegmentNormalizationOptions Default { get; } = new();
|
||||
|
||||
/// <summary>Minimal normalization (relocations only).</summary>
|
||||
public static ElfSegmentNormalizationOptions Minimal { get; } = new()
|
||||
{
|
||||
ZeroRelocations = true,
|
||||
CanonicalizeGotPlt = false,
|
||||
CanonicalizeNops = false,
|
||||
RewriteJumpTables = false,
|
||||
ZeroPadding = false
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of ELF segment normalization, including the normalized byte array
|
||||
/// and a deterministic delta hash.
|
||||
/// </summary>
|
||||
public sealed record ElfSegmentNormalizationResult
|
||||
{
|
||||
/// <summary>Normalized segment bytes (position-independent).</summary>
|
||||
public required ReadOnlyMemory<byte> NormalizedBytes { get; init; }
|
||||
|
||||
/// <summary>SHA-256 delta hash of the normalized bytes.</summary>
|
||||
public required string DeltaHash { get; init; }
|
||||
|
||||
/// <summary>Original segment size before normalization.</summary>
|
||||
public required int OriginalSize { get; init; }
|
||||
|
||||
/// <summary>Number of bytes that were modified.</summary>
|
||||
public required int ModifiedBytes { get; init; }
|
||||
|
||||
/// <summary>Normalization steps that were applied.</summary>
|
||||
public required ImmutableArray<ElfNormalizationStep> AppliedSteps { get; init; }
|
||||
|
||||
/// <summary>Per-step modification counts.</summary>
|
||||
public required ImmutableDictionary<ElfNormalizationStep, int> StepCounts { get; init; }
|
||||
|
||||
/// <summary>Ratio of modified bytes to original size [0.0, 1.0].</summary>
|
||||
public double ModificationRatio => OriginalSize == 0 ? 0.0 : (double)ModifiedBytes / OriginalSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes raw ELF binary segments by zeroing position-dependent bytes
|
||||
/// (relocations, GOT/PLT entries, absolute addresses) and canonicalizing
|
||||
/// NOP sleds, producing position-independent byte sequences suitable for
|
||||
/// deterministic delta hashing.
|
||||
/// </summary>
|
||||
public interface IElfSegmentNormalizer
|
||||
{
|
||||
/// <summary>
|
||||
/// Normalizes raw ELF segment bytes, removing position-dependent information.
|
||||
/// </summary>
|
||||
ElfSegmentNormalizationResult Normalize(
|
||||
ReadOnlySpan<byte> segmentBytes,
|
||||
ElfSegmentNormalizationOptions? options = null);
|
||||
|
||||
/// <summary>
|
||||
/// Computes a delta hash of normalized bytes for change comparison.
|
||||
/// </summary>
|
||||
string ComputeDeltaHash(ReadOnlySpan<byte> normalizedBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of ELF segment normalization.
|
||||
/// Operates on raw bytes without requiring a full ELF parser.
|
||||
/// </summary>
|
||||
public sealed class ElfSegmentNormalizer : IElfSegmentNormalizer
|
||||
{
|
||||
// ELF relocation entry sizes
|
||||
private const int Elf64RelEntrySize = 16; // Elf64_Rel
|
||||
private const int Elf64RelaEntrySize = 24; // Elf64_Rela
|
||||
|
||||
// x86/x64 NOP variants (multi-byte NOPs from Intel manuals)
|
||||
private static readonly byte[][] KnownNopPatterns =
|
||||
[
|
||||
[0x90], // NOP
|
||||
[0x66, 0x90], // 66 NOP
|
||||
[0x0F, 0x1F, 0x00], // NOP DWORD ptr [rax]
|
||||
[0x0F, 0x1F, 0x40, 0x00], // NOP DWORD ptr [rax+0]
|
||||
[0x0F, 0x1F, 0x44, 0x00, 0x00], // NOP DWORD ptr [rax+rax*1+0]
|
||||
[0x66, 0x0F, 0x1F, 0x44, 0x00, 0x00], // 66 NOP DWORD ptr [rax+rax*1+0]
|
||||
[0x0F, 0x1F, 0x80, 0x00, 0x00, 0x00, 0x00], // NOP DWORD ptr [rax+0x00000000]
|
||||
];
|
||||
|
||||
// PLT stub signature (push GOT entry, jmp resolver) — first 2 bytes of typical PLT entry
|
||||
private const byte PltPushOpcode = 0xFF;
|
||||
private const byte PltJmpOpcode = 0xFF;
|
||||
|
||||
// Canonical replacement for GOT/PLT entries (8 bytes of 0xCC — INT3 breakpoint)
|
||||
private static readonly byte[] CanonicalPltStub = [0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC];
|
||||
|
||||
private readonly Counter<long> _normalizeCounter;
|
||||
private readonly Counter<long> _bytesModifiedCounter;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new ELF segment normalizer with OTel instrumentation.
|
||||
/// </summary>
|
||||
public ElfSegmentNormalizer(IMeterFactory meterFactory)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(meterFactory);
|
||||
|
||||
var meter = meterFactory.Create("StellaOps.BinaryIndex.Normalization.ElfSegment");
|
||||
_normalizeCounter = meter.CreateCounter<long>("elfsegment.normalize.total", description: "Segments normalized");
|
||||
_bytesModifiedCounter = meter.CreateCounter<long>("elfsegment.bytes.modified", description: "Bytes modified during normalization");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ElfSegmentNormalizationResult Normalize(
|
||||
ReadOnlySpan<byte> segmentBytes,
|
||||
ElfSegmentNormalizationOptions? options = null)
|
||||
{
|
||||
options ??= ElfSegmentNormalizationOptions.Default;
|
||||
|
||||
var buffer = segmentBytes.ToArray();
|
||||
var totalModified = 0;
|
||||
var appliedSteps = ImmutableArray.CreateBuilder<ElfNormalizationStep>();
|
||||
var stepCounts = new Dictionary<ElfNormalizationStep, int>();
|
||||
|
||||
if (options.ZeroRelocations)
|
||||
{
|
||||
var count = ZeroRelocationEntries(buffer);
|
||||
if (count > 0)
|
||||
{
|
||||
appliedSteps.Add(ElfNormalizationStep.RelocationZeroing);
|
||||
stepCounts[ElfNormalizationStep.RelocationZeroing] = count;
|
||||
totalModified += count;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.CanonicalizeGotPlt)
|
||||
{
|
||||
var count = CanonicalizeGotPltEntries(buffer);
|
||||
if (count > 0)
|
||||
{
|
||||
appliedSteps.Add(ElfNormalizationStep.GotPltCanonicalization);
|
||||
stepCounts[ElfNormalizationStep.GotPltCanonicalization] = count;
|
||||
totalModified += count;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.CanonicalizeNops)
|
||||
{
|
||||
var count = CanonicalizeNopSleds(buffer);
|
||||
if (count > 0)
|
||||
{
|
||||
appliedSteps.Add(ElfNormalizationStep.NopCanonicalization);
|
||||
stepCounts[ElfNormalizationStep.NopCanonicalization] = count;
|
||||
totalModified += count;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.RewriteJumpTables)
|
||||
{
|
||||
var count = RewriteJumpTableEntries(buffer);
|
||||
if (count > 0)
|
||||
{
|
||||
appliedSteps.Add(ElfNormalizationStep.JumpTableRewriting);
|
||||
stepCounts[ElfNormalizationStep.JumpTableRewriting] = count;
|
||||
totalModified += count;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.ZeroPadding)
|
||||
{
|
||||
var count = ZeroAlignmentPadding(buffer);
|
||||
if (count > 0)
|
||||
{
|
||||
appliedSteps.Add(ElfNormalizationStep.PaddingZeroing);
|
||||
stepCounts[ElfNormalizationStep.PaddingZeroing] = count;
|
||||
totalModified += count;
|
||||
}
|
||||
}
|
||||
|
||||
var deltaHash = ComputeDeltaHash(buffer);
|
||||
|
||||
_normalizeCounter.Add(1);
|
||||
_bytesModifiedCounter.Add(totalModified);
|
||||
|
||||
return new ElfSegmentNormalizationResult
|
||||
{
|
||||
NormalizedBytes = buffer,
|
||||
DeltaHash = deltaHash,
|
||||
OriginalSize = segmentBytes.Length,
|
||||
ModifiedBytes = totalModified,
|
||||
AppliedSteps = appliedSteps.ToImmutable(),
|
||||
StepCounts = stepCounts.ToImmutableDictionary()
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string ComputeDeltaHash(ReadOnlySpan<byte> normalizedBytes)
|
||||
{
|
||||
Span<byte> hash = stackalloc byte[SHA256.HashSizeInBytes];
|
||||
SHA256.HashData(normalizedBytes, hash);
|
||||
return Convert.ToHexStringLower(hash);
|
||||
}
|
||||
|
||||
// ── Normalization passes ───────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Scans for ELF64 relocation entry patterns and zeros the address/addend fields.
|
||||
/// Heuristic: looks for 8-byte aligned blocks where the info field encodes a
|
||||
/// symbol index + relocation type consistent with common ELF patterns.
|
||||
/// </summary>
|
||||
internal static int ZeroRelocationEntries(byte[] buffer)
|
||||
{
|
||||
var zeroed = 0;
|
||||
|
||||
// Scan for RELA entries (24 bytes: offset[8] + info[8] + addend[8])
|
||||
// Zero the offset and addend fields which are position-dependent
|
||||
for (int i = 0; i + Elf64RelaEntrySize <= buffer.Length; i += 8)
|
||||
{
|
||||
if (!IsLikelyRelaEntry(buffer.AsSpan(i, Elf64RelaEntrySize)))
|
||||
continue;
|
||||
|
||||
// Zero the offset field (first 8 bytes)
|
||||
buffer.AsSpan(i, 8).Clear();
|
||||
// Zero the addend field (last 8 bytes)
|
||||
buffer.AsSpan(i + 16, 8).Clear();
|
||||
zeroed += 16;
|
||||
}
|
||||
|
||||
return zeroed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces GOT/PLT stub patterns with canonical stubs to eliminate
|
||||
/// position-dependent indirect jump targets.
|
||||
/// </summary>
|
||||
internal static int CanonicalizeGotPltEntries(byte[] buffer)
|
||||
{
|
||||
var modified = 0;
|
||||
|
||||
// Scan for PLT-style patterns: FF 25 xx xx xx xx (JMP [rip+disp32])
|
||||
// followed by FF 35 xx xx xx xx (PUSH [rip+disp32])
|
||||
for (int i = 0; i + 8 <= buffer.Length; i++)
|
||||
{
|
||||
if (buffer[i] == PltJmpOpcode && i + 6 <= buffer.Length &&
|
||||
(buffer[i + 1] == 0x25)) // JMP [rip+disp32]
|
||||
{
|
||||
// Zero the displacement (4 bytes after opcode+modrm)
|
||||
if (i + 6 <= buffer.Length)
|
||||
{
|
||||
buffer.AsSpan(i + 2, 4).Clear();
|
||||
modified += 4;
|
||||
}
|
||||
}
|
||||
else if (buffer[i] == PltPushOpcode && i + 6 <= buffer.Length &&
|
||||
(buffer[i + 1] == 0x35)) // PUSH [rip+disp32]
|
||||
{
|
||||
if (i + 6 <= buffer.Length)
|
||||
{
|
||||
buffer.AsSpan(i + 2, 4).Clear();
|
||||
modified += 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return modified;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces multi-byte NOP variants with canonical single-byte NOPs (0x90).
|
||||
/// </summary>
|
||||
internal static int CanonicalizeNopSleds(byte[] buffer)
|
||||
{
|
||||
var modified = 0;
|
||||
|
||||
for (int i = 0; i < buffer.Length;)
|
||||
{
|
||||
var matchLen = MatchNopPattern(buffer.AsSpan(i));
|
||||
if (matchLen > 1)
|
||||
{
|
||||
// Replace multi-byte NOP with canonical single-byte NOPs
|
||||
buffer.AsSpan(i, matchLen).Fill(0x90);
|
||||
modified += matchLen;
|
||||
i += matchLen;
|
||||
}
|
||||
else
|
||||
{
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
return modified;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rewrites jump table entries (arrays of absolute addresses used by
|
||||
/// switch statements) to zero, making them position-independent.
|
||||
/// Heuristic: scans for 8-byte aligned sequences of addresses that
|
||||
/// fall within the buffer's address range.
|
||||
/// </summary>
|
||||
internal static int RewriteJumpTableEntries(byte[] buffer)
|
||||
{
|
||||
var modified = 0;
|
||||
|
||||
// Look for sequences of 4+ consecutive 8-byte values that look like
|
||||
// code addresses (same upper 32 bits, varying lower 32 bits)
|
||||
for (int i = 0; i + 32 <= buffer.Length; i += 8)
|
||||
{
|
||||
if (!IsLikelyJumpTableStart(buffer.AsSpan(i, 32)))
|
||||
continue;
|
||||
|
||||
// Zero consecutive entries that share the same upper bits
|
||||
var upper = BinaryPrimitives.ReadUInt32LittleEndian(buffer.AsSpan(i + 4));
|
||||
var j = i;
|
||||
while (j + 8 <= buffer.Length)
|
||||
{
|
||||
var entryUpper = BinaryPrimitives.ReadUInt32LittleEndian(buffer.AsSpan(j + 4));
|
||||
if (entryUpper != upper) break;
|
||||
|
||||
buffer.AsSpan(j, 8).Clear();
|
||||
modified += 8;
|
||||
j += 8;
|
||||
}
|
||||
|
||||
i = j - 8; // skip past what we already processed
|
||||
}
|
||||
|
||||
return modified;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Zeros out alignment padding (0x00 or 0xCC sequences between code regions).
|
||||
/// </summary>
|
||||
internal static int ZeroAlignmentPadding(byte[] buffer)
|
||||
{
|
||||
var zeroed = 0;
|
||||
|
||||
// Look for runs of 0xCC (INT3) or 0x00 of 4+ bytes (alignment padding)
|
||||
for (int i = 0; i < buffer.Length;)
|
||||
{
|
||||
if (buffer[i] is 0xCC or 0x00)
|
||||
{
|
||||
var start = i;
|
||||
var padByte = buffer[i];
|
||||
while (i < buffer.Length && buffer[i] == padByte)
|
||||
i++;
|
||||
|
||||
var length = i - start;
|
||||
if (length >= 4)
|
||||
{
|
||||
buffer.AsSpan(start, length).Clear();
|
||||
zeroed += length;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
return zeroed;
|
||||
}
|
||||
|
||||
// ── Heuristic helpers ──────────────────────────────────────────────
|
||||
|
||||
private static bool IsLikelyRelaEntry(ReadOnlySpan<byte> data)
|
||||
{
|
||||
if (data.Length < Elf64RelaEntrySize) return false;
|
||||
|
||||
// Info field (bytes 8-15): upper 32 bits = symbol index, lower 32 = type
|
||||
var info = BinaryPrimitives.ReadUInt64LittleEndian(data[8..]);
|
||||
var relType = (uint)(info & 0xFFFFFFFF);
|
||||
var symIdx = (uint)(info >> 32);
|
||||
|
||||
// Common x86-64 relocation types: R_X86_64_GLOB_DAT(6), R_X86_64_JUMP_SLOT(7),
|
||||
// R_X86_64_RELATIVE(8), R_X86_64_64(1), R_X86_64_PC32(2)
|
||||
if (relType is 0 or > 42) return false; // outside valid range
|
||||
if (symIdx > 100_000) return false; // unreasonably large symbol index
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsLikelyJumpTableStart(ReadOnlySpan<byte> data)
|
||||
{
|
||||
if (data.Length < 32) return false;
|
||||
|
||||
// Check if 4 consecutive 8-byte values share the same upper 32 bits
|
||||
var upper0 = BinaryPrimitives.ReadUInt32LittleEndian(data[4..]);
|
||||
var upper1 = BinaryPrimitives.ReadUInt32LittleEndian(data[12..]);
|
||||
var upper2 = BinaryPrimitives.ReadUInt32LittleEndian(data[20..]);
|
||||
var upper3 = BinaryPrimitives.ReadUInt32LittleEndian(data[28..]);
|
||||
|
||||
return upper0 == upper1 && upper1 == upper2 && upper2 == upper3 && upper0 != 0;
|
||||
}
|
||||
|
||||
private static int MatchNopPattern(ReadOnlySpan<byte> data)
|
||||
{
|
||||
// Check longest patterns first for greedy matching
|
||||
for (int p = KnownNopPatterns.Length - 1; p >= 0; p--)
|
||||
{
|
||||
var pattern = KnownNopPatterns[p];
|
||||
if (pattern.Length > 1 && data.Length >= pattern.Length &&
|
||||
data[..pattern.Length].SequenceEqual(pattern))
|
||||
{
|
||||
return pattern.Length;
|
||||
}
|
||||
}
|
||||
|
||||
return data.Length > 0 && data[0] == 0x90 ? 1 : 0;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// Licensed under BUSL-1.1. See LICENSE in the project root.
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using StellaOps.BinaryIndex.Normalization.Arm64;
|
||||
using StellaOps.BinaryIndex.Normalization.X64;
|
||||
|
||||
@@ -26,6 +27,9 @@ public static class ServiceCollectionExtensions
|
||||
// Register the service that manages pipelines
|
||||
services.AddSingleton<NormalizationService>();
|
||||
|
||||
// Register ELF segment normalizer
|
||||
services.TryAddSingleton<IElfSegmentNormalizer, ElfSegmentNormalizer>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,634 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// CrossDistroCoverageTests.cs
|
||||
// Sprint: SPRINT_20260208_027_BinaryIndex_cross_distro_golden_set_for_backport_validation
|
||||
// Task: T1 — Tests for cross-distro coverage matrix models and service
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.Metrics;
|
||||
|
||||
using FluentAssertions;
|
||||
|
||||
using StellaOps.BinaryIndex.GoldenSet;
|
||||
|
||||
namespace StellaOps.BinaryIndex.GoldenSet.Tests.Unit;
|
||||
|
||||
file sealed class TestCrossDistroMeterFactory : IMeterFactory
|
||||
{
|
||||
private readonly List<Meter> _meters = [];
|
||||
public Meter Create(MeterOptions options)
|
||||
{
|
||||
var meter = new Meter(options);
|
||||
_meters.Add(meter);
|
||||
return meter;
|
||||
}
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var m in _meters) m.Dispose();
|
||||
_meters.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Model tests ────────────────────────────────────────────────────────
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class CrossDistroCoverageModelsTests
|
||||
{
|
||||
[Fact]
|
||||
public void DistroFamily_has_three_values()
|
||||
{
|
||||
Enum.GetValues<DistroFamily>().Should().HaveCount(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BackportStatus_has_four_values()
|
||||
{
|
||||
Enum.GetValues<BackportStatus>().Should().HaveCount(4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DistroCoverageEntry_roundtrips_properties()
|
||||
{
|
||||
// Arrange & Act
|
||||
var entry = new DistroCoverageEntry
|
||||
{
|
||||
Distro = DistroFamily.Debian,
|
||||
Version = "bookworm",
|
||||
PackageName = "openssl",
|
||||
PackageVersion = "3.0.11-1~deb12u1",
|
||||
Status = BackportStatus.Backported,
|
||||
Validated = true,
|
||||
ValidatedAt = DateTimeOffset.UtcNow,
|
||||
Notes = "Advisory DSA-5572-1"
|
||||
};
|
||||
|
||||
// Assert
|
||||
entry.Distro.Should().Be(DistroFamily.Debian);
|
||||
entry.Version.Should().Be("bookworm");
|
||||
entry.PackageName.Should().Be("openssl");
|
||||
entry.Status.Should().Be(BackportStatus.Backported);
|
||||
entry.Validated.Should().BeTrue();
|
||||
entry.ValidatedAt.Should().NotBeNull();
|
||||
entry.Notes.Should().Be("Advisory DSA-5572-1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DistroCoverageEntry_defaults()
|
||||
{
|
||||
// Arrange & Act
|
||||
var entry = new DistroCoverageEntry
|
||||
{
|
||||
Distro = DistroFamily.Alpine,
|
||||
Version = "3.18",
|
||||
PackageName = "curl",
|
||||
PackageVersion = "8.0",
|
||||
Status = BackportStatus.Unknown
|
||||
};
|
||||
|
||||
// Assert
|
||||
entry.Validated.Should().BeFalse();
|
||||
entry.ValidatedAt.Should().BeNull();
|
||||
entry.Notes.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CuratedCveEntry_computes_coverage_ratio()
|
||||
{
|
||||
// Arrange & Act
|
||||
var entry = new CuratedCveEntry
|
||||
{
|
||||
CveId = "CVE-2014-0160",
|
||||
Component = "openssl",
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
Coverage =
|
||||
[
|
||||
new DistroCoverageEntry
|
||||
{
|
||||
Distro = DistroFamily.Alpine, Version = "3.18", PackageName = "openssl",
|
||||
PackageVersion = "3.1.1", Status = BackportStatus.Backported, Validated = true
|
||||
},
|
||||
new DistroCoverageEntry
|
||||
{
|
||||
Distro = DistroFamily.Debian, Version = "bookworm", PackageName = "openssl",
|
||||
PackageVersion = "3.0.11", Status = BackportStatus.Backported, Validated = false
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
// Assert
|
||||
entry.TotalEntries.Should().Be(2);
|
||||
entry.ValidatedCount.Should().Be(1);
|
||||
entry.CoverageRatio.Should().BeApproximately(0.5, 0.001);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CuratedCveEntry_empty_coverage_returns_zero()
|
||||
{
|
||||
// Arrange & Act
|
||||
var entry = new CuratedCveEntry
|
||||
{
|
||||
CveId = "CVE-2000-0001",
|
||||
Component = "test",
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
Coverage = []
|
||||
};
|
||||
|
||||
// Assert
|
||||
entry.TotalEntries.Should().Be(0);
|
||||
entry.ValidatedCount.Should().Be(0);
|
||||
entry.CoverageRatio.Should().Be(0.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CuratedCveQuery_defaults()
|
||||
{
|
||||
// Arrange & Act
|
||||
var query = new CuratedCveQuery();
|
||||
|
||||
// Assert
|
||||
query.Component.Should().BeNull();
|
||||
query.Distro.Should().BeNull();
|
||||
query.Status.Should().BeNull();
|
||||
query.OnlyUnvalidated.Should().BeFalse();
|
||||
query.Limit.Should().Be(100);
|
||||
query.Offset.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CrossDistroCoverageSummary_computes_overall_coverage()
|
||||
{
|
||||
// Arrange & Act
|
||||
var summary = new CrossDistroCoverageSummary
|
||||
{
|
||||
TotalCves = 2,
|
||||
TotalEntries = 10,
|
||||
ValidatedEntries = 7,
|
||||
BackportedCount = 8,
|
||||
NotPatchedCount = 2,
|
||||
ByDistro = ImmutableDictionary<DistroFamily, DistroBreakdown>.Empty
|
||||
};
|
||||
|
||||
// Assert
|
||||
summary.OverallCoverage.Should().BeApproximately(0.7, 0.001);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CrossDistroCoverageSummary_empty_returns_zero()
|
||||
{
|
||||
// Arrange & Act
|
||||
var summary = new CrossDistroCoverageSummary
|
||||
{
|
||||
TotalCves = 0,
|
||||
TotalEntries = 0,
|
||||
ValidatedEntries = 0,
|
||||
BackportedCount = 0,
|
||||
NotPatchedCount = 0,
|
||||
ByDistro = ImmutableDictionary<DistroFamily, DistroBreakdown>.Empty
|
||||
};
|
||||
|
||||
// Assert
|
||||
summary.OverallCoverage.Should().Be(0.0);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Service tests ──────────────────────────────────────────────────────
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class CrossDistroCoverageServiceTests : IDisposable
|
||||
{
|
||||
private readonly TestCrossDistroMeterFactory _meterFactory = new();
|
||||
private readonly CrossDistroCoverageService _sut;
|
||||
|
||||
public CrossDistroCoverageServiceTests()
|
||||
{
|
||||
_sut = new CrossDistroCoverageService(_meterFactory);
|
||||
}
|
||||
|
||||
public void Dispose() => _meterFactory.Dispose();
|
||||
|
||||
// ── SeedBuiltInEntriesAsync ────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task SeedBuiltInEntries_populates_five_curated_cves()
|
||||
{
|
||||
// Act
|
||||
var seeded = await _sut.SeedBuiltInEntriesAsync();
|
||||
|
||||
// Assert
|
||||
seeded.Should().Be(5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SeedBuiltInEntries_is_idempotent()
|
||||
{
|
||||
// Arrange
|
||||
await _sut.SeedBuiltInEntriesAsync();
|
||||
|
||||
// Act
|
||||
var secondRun = await _sut.SeedBuiltInEntriesAsync();
|
||||
|
||||
// Assert
|
||||
secondRun.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SeedBuiltInEntries_includes_heartbleed()
|
||||
{
|
||||
// Arrange
|
||||
await _sut.SeedBuiltInEntriesAsync();
|
||||
|
||||
// Act
|
||||
var heartbleed = await _sut.GetByCveIdAsync("CVE-2014-0160");
|
||||
|
||||
// Assert
|
||||
heartbleed.Should().NotBeNull();
|
||||
heartbleed!.CommonName.Should().Be("Heartbleed");
|
||||
heartbleed.Component.Should().Be("openssl");
|
||||
heartbleed.Coverage.Should().HaveCountGreaterThanOrEqualTo(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SeedBuiltInEntries_includes_baron_samedit()
|
||||
{
|
||||
// Arrange
|
||||
await _sut.SeedBuiltInEntriesAsync();
|
||||
|
||||
// Act
|
||||
var baron = await _sut.GetByCveIdAsync("CVE-2021-3156");
|
||||
|
||||
// Assert
|
||||
baron.Should().NotBeNull();
|
||||
baron!.CommonName.Should().Be("Baron Samedit");
|
||||
baron.Component.Should().Be("sudo");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SeedBuiltInEntries_covers_all_three_distros()
|
||||
{
|
||||
// Arrange
|
||||
await _sut.SeedBuiltInEntriesAsync();
|
||||
|
||||
// Act
|
||||
var summary = await _sut.GetSummaryAsync();
|
||||
|
||||
// Assert
|
||||
summary.ByDistro.Should().ContainKey(DistroFamily.Alpine);
|
||||
summary.ByDistro.Should().ContainKey(DistroFamily.Debian);
|
||||
summary.ByDistro.Should().ContainKey(DistroFamily.Rhel);
|
||||
summary.ByDistro[DistroFamily.Alpine].EntryCount.Should().BeGreaterThan(0);
|
||||
summary.ByDistro[DistroFamily.Debian].EntryCount.Should().BeGreaterThan(0);
|
||||
summary.ByDistro[DistroFamily.Rhel].EntryCount.Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
// ── UpsertAsync ────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task UpsertAsync_stores_and_retrieves_entry()
|
||||
{
|
||||
// Arrange
|
||||
var entry = CreateEntry("CVE-2099-0001", "testlib");
|
||||
|
||||
// Act
|
||||
var result = await _sut.UpsertAsync(entry);
|
||||
var retrieved = await _sut.GetByCveIdAsync("CVE-2099-0001");
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.UpdatedAt.Should().NotBeNull();
|
||||
retrieved.Should().NotBeNull();
|
||||
retrieved!.Component.Should().Be("testlib");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpsertAsync_overwrites_existing()
|
||||
{
|
||||
// Arrange
|
||||
var entry1 = CreateEntry("CVE-2099-0001", "v1");
|
||||
var entry2 = CreateEntry("CVE-2099-0001", "v2");
|
||||
|
||||
// Act
|
||||
await _sut.UpsertAsync(entry1);
|
||||
await _sut.UpsertAsync(entry2);
|
||||
var retrieved = await _sut.GetByCveIdAsync("CVE-2099-0001");
|
||||
|
||||
// Assert
|
||||
retrieved.Should().NotBeNull();
|
||||
retrieved!.Component.Should().Be("v2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpsertAsync_throws_on_null()
|
||||
{
|
||||
// Act
|
||||
var act = () => _sut.UpsertAsync(null!);
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpsertAsync_throws_on_empty_cve_id()
|
||||
{
|
||||
// Arrange
|
||||
var entry = CreateEntry("", "test");
|
||||
|
||||
// Act
|
||||
var act = () => _sut.UpsertAsync(entry);
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<ArgumentException>();
|
||||
}
|
||||
|
||||
// ── GetByCveIdAsync ────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task GetByCveIdAsync_returns_null_for_unknown()
|
||||
{
|
||||
// Act
|
||||
var result = await _sut.GetByCveIdAsync("CVE-9999-0001");
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByCveIdAsync_is_case_insensitive()
|
||||
{
|
||||
// Arrange
|
||||
await _sut.UpsertAsync(CreateEntry("CVE-2099-0001", "test"));
|
||||
|
||||
// Act
|
||||
var result = await _sut.GetByCveIdAsync("cve-2099-0001");
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByCveIdAsync_throws_on_null()
|
||||
{
|
||||
// Act
|
||||
var act = () => _sut.GetByCveIdAsync(null!);
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<ArgumentException>();
|
||||
}
|
||||
|
||||
// ── QueryAsync ─────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task QueryAsync_returns_all_without_filters()
|
||||
{
|
||||
// Arrange
|
||||
await _sut.SeedBuiltInEntriesAsync();
|
||||
|
||||
// Act
|
||||
var results = await _sut.QueryAsync(new CuratedCveQuery());
|
||||
|
||||
// Assert
|
||||
results.Should().HaveCount(5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QueryAsync_filters_by_component()
|
||||
{
|
||||
// Arrange
|
||||
await _sut.SeedBuiltInEntriesAsync();
|
||||
|
||||
// Act
|
||||
var results = await _sut.QueryAsync(new CuratedCveQuery { Component = "openssl" });
|
||||
|
||||
// Assert
|
||||
results.Should().HaveCount(1);
|
||||
results[0].Component.Should().Be("openssl");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QueryAsync_filters_by_distro()
|
||||
{
|
||||
// Arrange
|
||||
await _sut.SeedBuiltInEntriesAsync();
|
||||
|
||||
// Act
|
||||
var results = await _sut.QueryAsync(new CuratedCveQuery { Distro = DistroFamily.Alpine });
|
||||
|
||||
// Assert
|
||||
results.Should().HaveCountGreaterThan(0);
|
||||
results.Should().OnlyContain(e => e.Coverage.Any(c => c.Distro == DistroFamily.Alpine));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QueryAsync_filters_by_status()
|
||||
{
|
||||
// Arrange
|
||||
await _sut.SeedBuiltInEntriesAsync();
|
||||
|
||||
// Act
|
||||
var results = await _sut.QueryAsync(new CuratedCveQuery { Status = BackportStatus.NotPatched });
|
||||
|
||||
// Assert
|
||||
results.Should().HaveCountGreaterThan(0);
|
||||
results.Should().OnlyContain(e => e.Coverage.Any(c => c.Status == BackportStatus.NotPatched));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QueryAsync_filters_only_unvalidated()
|
||||
{
|
||||
// Arrange
|
||||
await _sut.SeedBuiltInEntriesAsync();
|
||||
|
||||
// Act (all seeded entries start unvalidated)
|
||||
var results = await _sut.QueryAsync(new CuratedCveQuery { OnlyUnvalidated = true });
|
||||
|
||||
// Assert
|
||||
results.Should().HaveCount(5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QueryAsync_respects_limit_and_offset()
|
||||
{
|
||||
// Arrange
|
||||
await _sut.SeedBuiltInEntriesAsync();
|
||||
|
||||
// Act
|
||||
var page1 = await _sut.QueryAsync(new CuratedCveQuery { Limit = 2, Offset = 0 });
|
||||
var page2 = await _sut.QueryAsync(new CuratedCveQuery { Limit = 2, Offset = 2 });
|
||||
|
||||
// Assert
|
||||
page1.Should().HaveCount(2);
|
||||
page2.Should().HaveCount(2);
|
||||
page1[0].CveId.Should().NotBe(page2[0].CveId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QueryAsync_orders_by_cve_id()
|
||||
{
|
||||
// Arrange
|
||||
await _sut.SeedBuiltInEntriesAsync();
|
||||
|
||||
// Act
|
||||
var results = await _sut.QueryAsync(new CuratedCveQuery());
|
||||
|
||||
// Assert
|
||||
results.Should().BeInAscendingOrder(e => e.CveId, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
// ── GetSummaryAsync ────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task GetSummaryAsync_counts_all_entries()
|
||||
{
|
||||
// Arrange
|
||||
await _sut.SeedBuiltInEntriesAsync();
|
||||
|
||||
// Act
|
||||
var summary = await _sut.GetSummaryAsync();
|
||||
|
||||
// Assert
|
||||
summary.TotalCves.Should().Be(5);
|
||||
summary.TotalEntries.Should().BeGreaterThan(10);
|
||||
summary.ValidatedEntries.Should().Be(0); // none validated yet
|
||||
summary.BackportedCount.Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSummaryAsync_empty_store()
|
||||
{
|
||||
// Act
|
||||
var summary = await _sut.GetSummaryAsync();
|
||||
|
||||
// Assert
|
||||
summary.TotalCves.Should().Be(0);
|
||||
summary.TotalEntries.Should().Be(0);
|
||||
summary.OverallCoverage.Should().Be(0.0);
|
||||
}
|
||||
|
||||
// ── SetValidatedAsync ──────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task SetValidatedAsync_marks_entry_as_validated()
|
||||
{
|
||||
// Arrange
|
||||
await _sut.SeedBuiltInEntriesAsync();
|
||||
|
||||
// Act
|
||||
var result = await _sut.SetValidatedAsync("CVE-2014-0160", DistroFamily.Debian, "bookworm", true);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
var entry = await _sut.GetByCveIdAsync("CVE-2014-0160");
|
||||
entry.Should().NotBeNull();
|
||||
var debBookworm = entry!.Coverage.First(c => c.Distro == DistroFamily.Debian && c.Version == "bookworm");
|
||||
debBookworm.Validated.Should().BeTrue();
|
||||
debBookworm.ValidatedAt.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetValidatedAsync_returns_false_for_unknown_cve()
|
||||
{
|
||||
// Act
|
||||
var result = await _sut.SetValidatedAsync("CVE-9999-0001", DistroFamily.Alpine, "3.18", true);
|
||||
|
||||
// Assert
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetValidatedAsync_returns_false_for_unknown_distro_version()
|
||||
{
|
||||
// Arrange
|
||||
await _sut.SeedBuiltInEntriesAsync();
|
||||
|
||||
// Act
|
||||
var result = await _sut.SetValidatedAsync("CVE-2014-0160", DistroFamily.Alpine, "99.99", true);
|
||||
|
||||
// Assert
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetValidatedAsync_updates_summary_counts()
|
||||
{
|
||||
// Arrange
|
||||
await _sut.SeedBuiltInEntriesAsync();
|
||||
var beforeSummary = await _sut.GetSummaryAsync();
|
||||
|
||||
// Act
|
||||
await _sut.SetValidatedAsync("CVE-2014-0160", DistroFamily.Alpine, "3.18", true);
|
||||
var afterSummary = await _sut.GetSummaryAsync();
|
||||
|
||||
// Assert
|
||||
afterSummary.ValidatedEntries.Should().Be(beforeSummary.ValidatedEntries + 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetValidatedAsync_throws_on_null_cve_id()
|
||||
{
|
||||
// Act
|
||||
var act = () => _sut.SetValidatedAsync(null!, DistroFamily.Alpine, "3.18", true);
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<ArgumentException>();
|
||||
}
|
||||
|
||||
// ── CreateBuiltInEntries ───────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void CreateBuiltInEntries_is_deterministic()
|
||||
{
|
||||
// Arrange
|
||||
var timestamp = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
|
||||
// Act
|
||||
var entries1 = CrossDistroCoverageService.CreateBuiltInEntries(timestamp);
|
||||
var entries2 = CrossDistroCoverageService.CreateBuiltInEntries(timestamp);
|
||||
|
||||
// Assert
|
||||
entries1.Should().HaveCount(entries2.Length);
|
||||
for (int i = 0; i < entries1.Length; i++)
|
||||
{
|
||||
entries1[i].CveId.Should().Be(entries2[i].CveId);
|
||||
entries1[i].Component.Should().Be(entries2[i].Component);
|
||||
entries1[i].Coverage.Should().HaveCount(entries2[i].Coverage.Length);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateBuiltInEntries_covers_all_three_distro_families()
|
||||
{
|
||||
// Arrange
|
||||
var entries = CrossDistroCoverageService.CreateBuiltInEntries(DateTimeOffset.UtcNow);
|
||||
|
||||
// Act
|
||||
var allDistros = entries
|
||||
.SelectMany(e => e.Coverage)
|
||||
.Select(c => c.Distro)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
// Assert
|
||||
allDistros.Should().Contain(DistroFamily.Alpine);
|
||||
allDistros.Should().Contain(DistroFamily.Debian);
|
||||
allDistros.Should().Contain(DistroFamily.Rhel);
|
||||
}
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────
|
||||
|
||||
private static CuratedCveEntry CreateEntry(string cveId, string component) => new()
|
||||
{
|
||||
CveId = cveId,
|
||||
Component = component,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
Coverage =
|
||||
[
|
||||
new DistroCoverageEntry
|
||||
{
|
||||
Distro = DistroFamily.Alpine,
|
||||
Version = "3.18",
|
||||
PackageName = component,
|
||||
PackageVersion = "1.0.0-r0",
|
||||
Status = BackportStatus.Backported
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,578 @@
|
||||
// Copyright (c) StellaOps. All rights reserved.
|
||||
// Licensed under BUSL-1.1. See LICENSE in the project root.
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Diagnostics.Metrics;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace StellaOps.BinaryIndex.Normalization.Tests;
|
||||
|
||||
file sealed class TestElfMeterFactory : IMeterFactory
|
||||
{
|
||||
private readonly List<Meter> _meters = [];
|
||||
|
||||
public Meter Create(MeterOptions options)
|
||||
{
|
||||
var meter = new Meter(options);
|
||||
_meters.Add(meter);
|
||||
return meter;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var m in _meters) m.Dispose();
|
||||
_meters.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="ElfSegmentNormalizationOptions"/> and
|
||||
/// <see cref="ElfSegmentNormalizationResult"/> models.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public class ElfSegmentNormalizationModelTests
|
||||
{
|
||||
[Fact]
|
||||
public void DefaultOptions_EnablesAllNormalization()
|
||||
{
|
||||
var opts = ElfSegmentNormalizationOptions.Default;
|
||||
|
||||
opts.ZeroRelocations.Should().BeTrue();
|
||||
opts.CanonicalizeGotPlt.Should().BeTrue();
|
||||
opts.CanonicalizeNops.Should().BeTrue();
|
||||
opts.RewriteJumpTables.Should().BeTrue();
|
||||
opts.ZeroPadding.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinimalOptions_OnlyRelocations()
|
||||
{
|
||||
var opts = ElfSegmentNormalizationOptions.Minimal;
|
||||
|
||||
opts.ZeroRelocations.Should().BeTrue();
|
||||
opts.CanonicalizeGotPlt.Should().BeFalse();
|
||||
opts.CanonicalizeNops.Should().BeFalse();
|
||||
opts.RewriteJumpTables.Should().BeFalse();
|
||||
opts.ZeroPadding.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Result_ModificationRatio_ZeroForEmpty()
|
||||
{
|
||||
var result = new ElfSegmentNormalizationResult
|
||||
{
|
||||
NormalizedBytes = Array.Empty<byte>(),
|
||||
DeltaHash = "abc",
|
||||
OriginalSize = 0,
|
||||
ModifiedBytes = 0,
|
||||
AppliedSteps = [],
|
||||
StepCounts = System.Collections.Immutable.ImmutableDictionary<ElfNormalizationStep, int>.Empty
|
||||
};
|
||||
|
||||
result.ModificationRatio.Should().Be(0.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Result_ModificationRatio_ComputedCorrectly()
|
||||
{
|
||||
var result = new ElfSegmentNormalizationResult
|
||||
{
|
||||
NormalizedBytes = new byte[80],
|
||||
DeltaHash = "abc",
|
||||
OriginalSize = 100,
|
||||
ModifiedBytes = 25,
|
||||
AppliedSteps = [ElfNormalizationStep.RelocationZeroing],
|
||||
StepCounts = new Dictionary<ElfNormalizationStep, int>
|
||||
{
|
||||
[ElfNormalizationStep.RelocationZeroing] = 25
|
||||
}.ToImmutableDictionary()
|
||||
};
|
||||
|
||||
result.ModificationRatio.Should().BeApproximately(0.25, 0.001);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalizationStep_EnumValues()
|
||||
{
|
||||
((int)ElfNormalizationStep.RelocationZeroing).Should().Be(0);
|
||||
((int)ElfNormalizationStep.GotPltCanonicalization).Should().Be(1);
|
||||
((int)ElfNormalizationStep.NopCanonicalization).Should().Be(2);
|
||||
((int)ElfNormalizationStep.JumpTableRewriting).Should().Be(3);
|
||||
((int)ElfNormalizationStep.PaddingZeroing).Should().Be(4);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="ElfSegmentNormalizer"/> service.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public class ElfSegmentNormalizerTests : IDisposable
|
||||
{
|
||||
private readonly TestElfMeterFactory _meterFactory = new();
|
||||
private readonly ElfSegmentNormalizer _normalizer;
|
||||
|
||||
public ElfSegmentNormalizerTests()
|
||||
{
|
||||
_normalizer = new ElfSegmentNormalizer(_meterFactory);
|
||||
}
|
||||
|
||||
public void Dispose() => _meterFactory.Dispose();
|
||||
|
||||
// ── Constructor ────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NullMeterFactory_Throws()
|
||||
{
|
||||
var act = () => new ElfSegmentNormalizer(null!);
|
||||
act.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
// ── Empty input ────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Normalize_EmptyInput_ReturnsDeterministicResult()
|
||||
{
|
||||
var result = _normalizer.Normalize(ReadOnlySpan<byte>.Empty);
|
||||
|
||||
result.NormalizedBytes.Length.Should().Be(0);
|
||||
result.OriginalSize.Should().Be(0);
|
||||
result.ModifiedBytes.Should().Be(0);
|
||||
result.AppliedSteps.Should().BeEmpty();
|
||||
result.DeltaHash.Should().NotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_EmptyInput_HashIsSha256OfEmpty()
|
||||
{
|
||||
var result = _normalizer.Normalize(ReadOnlySpan<byte>.Empty);
|
||||
|
||||
// SHA-256 of empty input
|
||||
result.DeltaHash.Should().Be(
|
||||
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
|
||||
}
|
||||
|
||||
// ── DeltaHash ──────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ComputeDeltaHash_Deterministic()
|
||||
{
|
||||
byte[] input = [0x01, 0x02, 0x03, 0x04];
|
||||
|
||||
var hash1 = _normalizer.ComputeDeltaHash(input);
|
||||
var hash2 = _normalizer.ComputeDeltaHash(input);
|
||||
|
||||
hash1.Should().Be(hash2);
|
||||
hash1.Should().HaveLength(64); // SHA-256 hex string
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeDeltaHash_DifferentInputs_DifferentHash()
|
||||
{
|
||||
byte[] a = [0x01, 0x02, 0x03];
|
||||
byte[] b = [0x04, 0x05, 0x06];
|
||||
|
||||
_normalizer.ComputeDeltaHash(a).Should().NotBe(_normalizer.ComputeDeltaHash(b));
|
||||
}
|
||||
|
||||
// ── NOP canonicalization ───────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Normalize_MultiByteNops_CanonicalizedToSingleByteNops()
|
||||
{
|
||||
// 3-byte NOP: 0F 1F 00
|
||||
byte[] input = [0x0F, 0x1F, 0x00, 0xCC, 0xCC, 0xCC, 0xCC];
|
||||
var opts = new ElfSegmentNormalizationOptions
|
||||
{
|
||||
CanonicalizeNops = true,
|
||||
ZeroRelocations = false,
|
||||
CanonicalizeGotPlt = false,
|
||||
RewriteJumpTables = false,
|
||||
ZeroPadding = false
|
||||
};
|
||||
|
||||
var result = _normalizer.Normalize(input, opts);
|
||||
|
||||
// The 3-byte NOP should become 3x 0x90
|
||||
result.NormalizedBytes.Span[0].Should().Be(0x90);
|
||||
result.NormalizedBytes.Span[1].Should().Be(0x90);
|
||||
result.NormalizedBytes.Span[2].Should().Be(0x90);
|
||||
result.AppliedSteps.Should().Contain(ElfNormalizationStep.NopCanonicalization);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_TwoByteNop_CanonicalizedToSingleByteNops()
|
||||
{
|
||||
// 2-byte NOP: 66 90
|
||||
byte[] input = [0x66, 0x90, 0xAB, 0xCD];
|
||||
var opts = new ElfSegmentNormalizationOptions
|
||||
{
|
||||
CanonicalizeNops = true,
|
||||
ZeroRelocations = false,
|
||||
CanonicalizeGotPlt = false,
|
||||
RewriteJumpTables = false,
|
||||
ZeroPadding = false
|
||||
};
|
||||
|
||||
var result = _normalizer.Normalize(input, opts);
|
||||
|
||||
result.NormalizedBytes.Span[0].Should().Be(0x90);
|
||||
result.NormalizedBytes.Span[1].Should().Be(0x90);
|
||||
// Non-NOP bytes should be unchanged
|
||||
result.NormalizedBytes.Span[2].Should().Be(0xAB);
|
||||
result.NormalizedBytes.Span[3].Should().Be(0xCD);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_FourByteNop_CanonicalizedToSingleByteNops()
|
||||
{
|
||||
// 4-byte NOP: 0F 1F 40 00
|
||||
byte[] input = [0x0F, 0x1F, 0x40, 0x00];
|
||||
var opts = new ElfSegmentNormalizationOptions
|
||||
{
|
||||
CanonicalizeNops = true,
|
||||
ZeroRelocations = false,
|
||||
CanonicalizeGotPlt = false,
|
||||
RewriteJumpTables = false,
|
||||
ZeroPadding = false
|
||||
};
|
||||
|
||||
var result = _normalizer.Normalize(input, opts);
|
||||
|
||||
result.NormalizedBytes.Span.ToArray().Should().AllBeEquivalentTo((byte)0x90);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_NoNops_NoNopStep()
|
||||
{
|
||||
byte[] input = [0xE8, 0x10, 0x20, 0x30, 0x40]; // CALL instruction
|
||||
var opts = new ElfSegmentNormalizationOptions
|
||||
{
|
||||
CanonicalizeNops = true,
|
||||
ZeroRelocations = false,
|
||||
CanonicalizeGotPlt = false,
|
||||
RewriteJumpTables = false,
|
||||
ZeroPadding = false
|
||||
};
|
||||
|
||||
var result = _normalizer.Normalize(input, opts);
|
||||
|
||||
result.AppliedSteps.Should().NotContain(ElfNormalizationStep.NopCanonicalization);
|
||||
}
|
||||
|
||||
// ── GOT/PLT canonicalization ───────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Normalize_PltJmpPattern_DisplacementZeroed()
|
||||
{
|
||||
// FF 25 xx xx xx xx — JMP [rip+disp32]
|
||||
byte[] input = [0xFF, 0x25, 0xDE, 0xAD, 0xBE, 0xEF, 0xAA, 0xBB];
|
||||
var opts = new ElfSegmentNormalizationOptions
|
||||
{
|
||||
CanonicalizeGotPlt = true,
|
||||
ZeroRelocations = false,
|
||||
CanonicalizeNops = false,
|
||||
RewriteJumpTables = false,
|
||||
ZeroPadding = false
|
||||
};
|
||||
|
||||
var result = _normalizer.Normalize(input, opts);
|
||||
|
||||
// Displacement bytes should be zeroed
|
||||
result.NormalizedBytes.Span[0].Should().Be(0xFF);
|
||||
result.NormalizedBytes.Span[1].Should().Be(0x25);
|
||||
result.NormalizedBytes.Span[2].Should().Be(0x00);
|
||||
result.NormalizedBytes.Span[3].Should().Be(0x00);
|
||||
result.NormalizedBytes.Span[4].Should().Be(0x00);
|
||||
result.NormalizedBytes.Span[5].Should().Be(0x00);
|
||||
result.AppliedSteps.Should().Contain(ElfNormalizationStep.GotPltCanonicalization);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_PltPushPattern_DisplacementZeroed()
|
||||
{
|
||||
// FF 35 xx xx xx xx — PUSH [rip+disp32]
|
||||
byte[] input = [0xFF, 0x35, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66];
|
||||
var opts = new ElfSegmentNormalizationOptions
|
||||
{
|
||||
CanonicalizeGotPlt = true,
|
||||
ZeroRelocations = false,
|
||||
CanonicalizeNops = false,
|
||||
RewriteJumpTables = false,
|
||||
ZeroPadding = false
|
||||
};
|
||||
|
||||
var result = _normalizer.Normalize(input, opts);
|
||||
|
||||
result.NormalizedBytes.Span[2].Should().Be(0x00);
|
||||
result.NormalizedBytes.Span[3].Should().Be(0x00);
|
||||
result.NormalizedBytes.Span[4].Should().Be(0x00);
|
||||
result.NormalizedBytes.Span[5].Should().Be(0x00);
|
||||
}
|
||||
|
||||
// ── Alignment padding ──────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Normalize_Int3Padding_Zeroed()
|
||||
{
|
||||
// 4+ bytes of 0xCC (INT3) = alignment padding
|
||||
byte[] input = [0xE8, 0x01, 0xCC, 0xCC, 0xCC, 0xCC, 0xE9, 0x02];
|
||||
var opts = new ElfSegmentNormalizationOptions
|
||||
{
|
||||
ZeroPadding = true,
|
||||
ZeroRelocations = false,
|
||||
CanonicalizeGotPlt = false,
|
||||
CanonicalizeNops = false,
|
||||
RewriteJumpTables = false
|
||||
};
|
||||
|
||||
var result = _normalizer.Normalize(input, opts);
|
||||
|
||||
// INT3 padding should be zeroed
|
||||
result.NormalizedBytes.Span[2].Should().Be(0x00);
|
||||
result.NormalizedBytes.Span[3].Should().Be(0x00);
|
||||
result.NormalizedBytes.Span[4].Should().Be(0x00);
|
||||
result.NormalizedBytes.Span[5].Should().Be(0x00);
|
||||
// Non-padding bytes should be preserved
|
||||
result.NormalizedBytes.Span[0].Should().Be(0xE8);
|
||||
result.NormalizedBytes.Span[6].Should().Be(0xE9);
|
||||
result.AppliedSteps.Should().Contain(ElfNormalizationStep.PaddingZeroing);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_ShortPadding_NotZeroed()
|
||||
{
|
||||
// Less than 4 bytes of 0xCC should not be treated as padding
|
||||
byte[] input = [0xCC, 0xCC, 0xCC, 0xE8];
|
||||
var opts = new ElfSegmentNormalizationOptions
|
||||
{
|
||||
ZeroPadding = true,
|
||||
ZeroRelocations = false,
|
||||
CanonicalizeGotPlt = false,
|
||||
CanonicalizeNops = false,
|
||||
RewriteJumpTables = false
|
||||
};
|
||||
|
||||
var result = _normalizer.Normalize(input, opts);
|
||||
|
||||
// 3 bytes of 0xCC is below the padding threshold (4)
|
||||
result.NormalizedBytes.Span[0].Should().Be(0xCC);
|
||||
result.NormalizedBytes.Span[1].Should().Be(0xCC);
|
||||
result.NormalizedBytes.Span[2].Should().Be(0xCC);
|
||||
}
|
||||
|
||||
// ── Relocation zeroing ─────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ZeroRelocationEntries_ValidRelaEntry_ZerosOffsetAndAddend()
|
||||
{
|
||||
// Build a RELA entry: offset[8] + info[8] + addend[8] = 24 bytes
|
||||
// Info = (symbolIndex=1 << 32) | relType=7 (R_X86_64_JUMP_SLOT)
|
||||
var buffer = new byte[24];
|
||||
// offset
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(buffer.AsSpan(0), 0x00400000);
|
||||
// info: sym=1, type=7
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(buffer.AsSpan(8), (1UL << 32) | 7);
|
||||
// addend
|
||||
BinaryPrimitives.WriteInt64LittleEndian(buffer.AsSpan(16), 0x1234);
|
||||
|
||||
var zeroed = ElfSegmentNormalizer.ZeroRelocationEntries(buffer);
|
||||
|
||||
zeroed.Should().BeGreaterThan(0);
|
||||
// Offset field should be zeroed
|
||||
BinaryPrimitives.ReadUInt64LittleEndian(buffer.AsSpan(0)).Should().Be(0);
|
||||
// Addend field should be zeroed
|
||||
BinaryPrimitives.ReadInt64LittleEndian(buffer.AsSpan(16)).Should().Be(0);
|
||||
// Info field should be preserved
|
||||
BinaryPrimitives.ReadUInt64LittleEndian(buffer.AsSpan(8)).Should().NotBe(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZeroRelocationEntries_InvalidEntry_NotZeroed()
|
||||
{
|
||||
// Build data that doesn't look like a relocation entry
|
||||
// (relType > 42 or symIdx > 100_000)
|
||||
var buffer = new byte[24];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(buffer.AsSpan(0), 0xDEADBEEF);
|
||||
// info with invalid rel type (99)
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(buffer.AsSpan(8), 99UL);
|
||||
BinaryPrimitives.WriteInt64LittleEndian(buffer.AsSpan(16), 0x5678);
|
||||
|
||||
var zeroed = ElfSegmentNormalizer.ZeroRelocationEntries(buffer);
|
||||
|
||||
zeroed.Should().Be(0);
|
||||
// Original data should be preserved
|
||||
BinaryPrimitives.ReadUInt64LittleEndian(buffer.AsSpan(0)).Should().Be(0xDEADBEEF);
|
||||
}
|
||||
|
||||
// ── NOP canonicalization (static) ──────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void CanonicalizeNopSleds_SevenByteNop_AllBecome0x90()
|
||||
{
|
||||
byte[] buffer = [0x0F, 0x1F, 0x80, 0x00, 0x00, 0x00, 0x00, 0xFF];
|
||||
|
||||
var count = ElfSegmentNormalizer.CanonicalizeNopSleds(buffer);
|
||||
|
||||
count.Should().Be(7); // 7-byte NOP replaced
|
||||
buffer[0].Should().Be(0x90);
|
||||
buffer[6].Should().Be(0x90);
|
||||
buffer[7].Should().Be(0xFF); // non-NOP preserved
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanonicalizeNopSleds_SingleByteNop_NotModified()
|
||||
{
|
||||
byte[] buffer = [0x90, 0xAA];
|
||||
|
||||
var count = ElfSegmentNormalizer.CanonicalizeNopSleds(buffer);
|
||||
|
||||
count.Should().Be(0); // single-byte NOP is already canonical
|
||||
buffer[0].Should().Be(0x90);
|
||||
}
|
||||
|
||||
// ── GOT/PLT canonicalization (static) ──────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void CanonicalizeGotPltEntries_JmpRipDisp32_ZerosDisplacement()
|
||||
{
|
||||
byte[] buffer = [0xFF, 0x25, 0xAA, 0xBB, 0xCC, 0xDD, 0x90];
|
||||
|
||||
var count = ElfSegmentNormalizer.CanonicalizeGotPltEntries(buffer);
|
||||
|
||||
count.Should().Be(4);
|
||||
buffer[2].Should().Be(0);
|
||||
buffer[3].Should().Be(0);
|
||||
buffer[4].Should().Be(0);
|
||||
buffer[5].Should().Be(0);
|
||||
}
|
||||
|
||||
// ── Alignment padding (static) ─────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ZeroAlignmentPadding_FourPlusInt3Bytes_Zeroed()
|
||||
{
|
||||
byte[] buffer = [0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xAA];
|
||||
|
||||
var count = ElfSegmentNormalizer.ZeroAlignmentPadding(buffer);
|
||||
|
||||
count.Should().Be(5);
|
||||
buffer[0].Should().Be(0);
|
||||
buffer[4].Should().Be(0);
|
||||
buffer[5].Should().Be(0xAA);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZeroAlignmentPadding_ZeroByteRun_Zeroed()
|
||||
{
|
||||
byte[] buffer = [0x00, 0x00, 0x00, 0x00, 0xBB];
|
||||
|
||||
var count = ElfSegmentNormalizer.ZeroAlignmentPadding(buffer);
|
||||
|
||||
count.Should().Be(4);
|
||||
}
|
||||
|
||||
// ── Full pipeline ──────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Normalize_AllStepsEnabled_DeterministicHash()
|
||||
{
|
||||
byte[] input = [0x0F, 0x1F, 0x00, 0xFF, 0x25, 0x11, 0x22, 0x33, 0x44, 0xCC, 0xCC, 0xCC, 0xCC];
|
||||
|
||||
var result1 = _normalizer.Normalize(input);
|
||||
var result2 = _normalizer.Normalize(input);
|
||||
|
||||
result1.DeltaHash.Should().Be(result2.DeltaHash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_MinimalVsDefault_ProducesDifferentHashes()
|
||||
{
|
||||
// Input with NOP + PLT + padding
|
||||
byte[] input = [0x0F, 0x1F, 0x00, 0xFF, 0x25, 0x11, 0x22, 0x33, 0x44, 0xCC, 0xCC, 0xCC, 0xCC];
|
||||
|
||||
var defaultResult = _normalizer.Normalize(input, ElfSegmentNormalizationOptions.Default);
|
||||
var minimalResult = _normalizer.Normalize(input, ElfSegmentNormalizationOptions.Minimal);
|
||||
|
||||
// With more normalization steps, the results should differ
|
||||
defaultResult.DeltaHash.Should().NotBe(minimalResult.DeltaHash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_AllDisabled_NoModifications()
|
||||
{
|
||||
byte[] input = [0x0F, 0x1F, 0x00, 0xFF, 0x25, 0x11, 0x22, 0x33, 0x44];
|
||||
var opts = new ElfSegmentNormalizationOptions
|
||||
{
|
||||
ZeroRelocations = false,
|
||||
CanonicalizeGotPlt = false,
|
||||
CanonicalizeNops = false,
|
||||
RewriteJumpTables = false,
|
||||
ZeroPadding = false
|
||||
};
|
||||
|
||||
var result = _normalizer.Normalize(input, opts);
|
||||
|
||||
result.ModifiedBytes.Should().Be(0);
|
||||
result.AppliedSteps.Should().BeEmpty();
|
||||
result.NormalizedBytes.Span.ToArray().Should().Equal(input);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_StepCountsMatchAppliedSteps()
|
||||
{
|
||||
byte[] input = [0x0F, 0x1F, 0x00, 0xCC, 0xCC, 0xCC, 0xCC];
|
||||
var opts = new ElfSegmentNormalizationOptions
|
||||
{
|
||||
CanonicalizeNops = true,
|
||||
ZeroPadding = true,
|
||||
ZeroRelocations = false,
|
||||
CanonicalizeGotPlt = false,
|
||||
RewriteJumpTables = false
|
||||
};
|
||||
|
||||
var result = _normalizer.Normalize(input, opts);
|
||||
|
||||
foreach (var step in result.AppliedSteps)
|
||||
{
|
||||
result.StepCounts.Should().ContainKey(step);
|
||||
result.StepCounts[step].Should().BeGreaterThan(0);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Jump table rewriting (static) ──────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void RewriteJumpTableEntries_ConsecutiveAddresses_Zeroed()
|
||||
{
|
||||
// Build 4 consecutive 8-byte entries with same upper 32 bits
|
||||
var buffer = new byte[32];
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(i * 8), (uint)(0x1000 + i * 16));
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(i * 8 + 4), 0x00400000);
|
||||
}
|
||||
|
||||
var count = ElfSegmentNormalizer.RewriteJumpTableEntries(buffer);
|
||||
|
||||
count.Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RewriteJumpTableEntries_RandomData_NotModified()
|
||||
{
|
||||
// Random data that shouldn't look like a jump table
|
||||
byte[] buffer =
|
||||
[
|
||||
0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0,
|
||||
0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22,
|
||||
0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00,
|
||||
0xA1, 0xB2, 0xC3, 0xD4, 0xE5, 0xF6, 0x07, 0x18
|
||||
];
|
||||
|
||||
var original = buffer.ToArray();
|
||||
var count = ElfSegmentNormalizer.RewriteJumpTableEntries(buffer);
|
||||
|
||||
count.Should().Be(0);
|
||||
buffer.Should().Equal(original);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user