using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging.Abstractions; using StellaOps.Scanner.Analyzers.OS; using StellaOps.Scanner.Analyzers.OS.Apk; using StellaOps.Scanner.Analyzers.OS.Dpkg; using StellaOps.Scanner.Analyzers.OS.Rpm; using StellaOps.Scanner.Analyzers.OS.Rpm.Internal; using StellaOps.Scanner.Core.Contracts; using StellaOps.Scanner.Analyzers.OS.Tests.TestUtilities; using Xunit; namespace StellaOps.Scanner.Analyzers.OS.Tests; public sealed class OsAnalyzerDeterminismTests { [Trait("Category", TestCategories.Unit)] [Fact] public async Task ApkAnalyzerMatchesGolden() { using var fixture = FixtureManager.UseFixture("apk", out var rootPath); var analyzer = new ApkPackageAnalyzer(NullLogger.Instance); var context = CreateContext(rootPath); var result = await analyzer.AnalyzeAsync(context, CancellationToken.None); var snapshot = SnapshotSerializer.Serialize(new[] { result }); GoldenAssert.MatchSnapshot(snapshot, FixtureManager.GetGoldenPath("apk.json")); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task DpkgAnalyzerMatchesGolden() { using var fixture = FixtureManager.UseFixture("dpkg", out var rootPath); using StellaOps.TestKit; var analyzer = new DpkgPackageAnalyzer(NullLogger.Instance); var context = CreateContext(rootPath); var result = await analyzer.AnalyzeAsync(context, CancellationToken.None); var snapshot = SnapshotSerializer.Serialize(new[] { result }); GoldenAssert.MatchSnapshot(snapshot, FixtureManager.GetGoldenPath("dpkg.json")); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task RpmAnalyzerMatchesGolden() { var headers = new[] { CreateRpmHeader( name: "openssl-libs", version: "3.2.1", architecture: "x86_64", release: "8.el9", epoch: "1", license: "OpenSSL", sourceRpm: "openssl-3.2.1-8.el9.src.rpm", provides: new[] { "libcrypto.so.3()(64bit)", "openssl-libs" }, requires: new[] { "glibc(x86-64) >= 2.34" }, files: new[] { new RpmFileEntry("/usr/lib64/libcrypto.so.3", false, new Dictionary { ["sha256"] = "abc123" }), new RpmFileEntry("/etc/pki/tls/openssl.cnf", true, new Dictionary { ["md5"] = "c0ffee" }) }, changeLogs: new[] { "Resolves: CVE-2025-1234" }, metadata: new Dictionary { ["summary"] = "TLS toolkit" }) }; var reader = new StubRpmDatabaseReader(headers); var analyzer = new RpmPackageAnalyzer( NullLogger.Instance, reader); var context = CreateContext("/tmp/nonexistent"); var result = await analyzer.AnalyzeAsync(context, CancellationToken.None); var snapshot = SnapshotSerializer.Serialize(new[] { result }); GoldenAssert.MatchSnapshot(snapshot, FixtureManager.GetGoldenPath("rpm.json")); } private static OSPackageAnalyzerContext CreateContext(string rootPath) { var metadata = new Dictionary { [ScanMetadataKeys.RootFilesystemPath] = rootPath }; return new OSPackageAnalyzerContext(rootPath, workspacePath: null, TimeProvider.System, NullLoggerFactory.Instance.CreateLogger("os-analyzer-tests"), metadata); } private static RpmHeader CreateRpmHeader( string name, string version, string architecture, string? release, string? epoch, string? license, string? sourceRpm, IReadOnlyList provides, IReadOnlyList requires, IReadOnlyList files, IReadOnlyList changeLogs, IReadOnlyDictionary metadata) { return new RpmHeader( name, version, architecture, release, epoch, metadata.TryGetValue("summary", out var summary) ? summary : null, metadata.TryGetValue("description", out var description) ? description : null, license, sourceRpm, metadata.TryGetValue("url", out var url) ? url : null, metadata.TryGetValue("vendor", out var vendor) ? vendor : null, buildTime: null, installTime: null, provides, provideVersions: provides.Select(_ => string.Empty).ToArray(), requires, requireVersions: requires.Select(_ => string.Empty).ToArray(), files, changeLogs, metadata); } private sealed class StubRpmDatabaseReader : IRpmDatabaseReader { private readonly IReadOnlyList _headers; public StubRpmDatabaseReader(IReadOnlyList headers) { _headers = headers; } public IReadOnlyList ReadHeaders(string rootPath, CancellationToken cancellationToken) => _headers; } }