up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-13 00:20:26 +02:00
parent e1f1bef4c1
commit 564df71bfb
2376 changed files with 334389 additions and 328032 deletions

View File

@@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.IO;
using StellaOps.Scanner.Analyzers.OS.Helpers;
using Xunit;
namespace StellaOps.Scanner.Analyzers.OS.Tests.Helpers;
public sealed class OsFileEvidenceFactoryTests
{
[Fact]
public void Create_DoesNotComputeSha256_WhenOtherDigestsPresent()
{
var rootPath = Path.Combine(Path.GetTempPath(), "stellaops-os-evidence-" + Guid.NewGuid().ToString("N")[..8]);
var filePath = Path.Combine(rootPath, "bin", "test");
Directory.CreateDirectory(Path.GetDirectoryName(filePath)!);
File.WriteAllText(filePath, "hello");
try
{
var metadata = new Dictionary<string, string>(StringComparer.Ordinal);
var factory = OsFileEvidenceFactory.Create(rootPath, metadata);
var evidence = factory.Create(
"bin/test",
isConfigFile: false,
digests: new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["md5"] = "deadbeef"
});
Assert.NotNull(evidence);
Assert.Null(evidence.Sha256);
Assert.True(evidence.Digests.ContainsKey("md5"));
Assert.False(evidence.Digests.ContainsKey("sha256"));
Assert.True(evidence.SizeBytes.HasValue);
}
finally
{
Directory.Delete(rootPath, recursive: true);
}
}
}

View File

@@ -1,76 +1,76 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using StellaOps.Scanner.Analyzers.OS.Mapping;
using StellaOps.Scanner.Core.Contracts;
using Xunit;
namespace StellaOps.Scanner.Analyzers.OS.Tests.Mapping;
public class OsComponentMapperTests
{
[Fact]
public void ToLayerFragments_ProducesDeterministicComponents()
{
var package = new OSPackageRecord(
analyzerId: "apk",
packageUrl: "pkg:alpine/busybox@1.37.0-r0?arch=x86_64",
name: "busybox",
version: "1.37.0",
architecture: "x86_64",
evidenceSource: PackageEvidenceSource.ApkDatabase,
release: "r0",
sourcePackage: "busybox",
license: "GPL-2.0-only",
depends: new[] { "musl>=1.2.5-r0", "ssl_client" },
files: new[]
{
new OSPackageFileEvidence("/bin/busybox", sha256: "abc123", isConfigFile: false),
new OSPackageFileEvidence("/etc/profile", isConfigFile: true, digests: new Dictionary<string, string> { ["md5"] = "deadbeef" }),
},
vendorMetadata: new Dictionary<string, string?>
{
["homepage"] = "https://busybox.net/",
});
var result = new OSPackageAnalyzerResult(
analyzerId: "apk",
packages: ImmutableArray.Create(package),
telemetry: new OSAnalyzerTelemetry(System.TimeSpan.Zero, 1, 2));
var fragments = OsComponentMapper.ToLayerFragments(new[] { result });
Assert.Single(fragments);
var fragment = fragments[0];
Assert.StartsWith("sha256:", fragment.LayerDigest);
Assert.Single(fragment.Components);
var component = fragment.Components[0];
Assert.Equal(fragment.LayerDigest, component.LayerDigest);
Assert.Equal("pkg:alpine/busybox@1.37.0-r0?arch=x86_64", component.Identity.Key);
Assert.Equal("busybox", component.Identity.Name);
Assert.Equal("1.37.0", component.Identity.Version);
Assert.Equal("pkg:alpine/busybox@1.37.0-r0?arch=x86_64", component.Identity.Purl);
Assert.Equal("os-package", component.Identity.ComponentType);
Assert.Equal("busybox", component.Identity.Group);
Assert.Collection(component.Evidence,
evidence =>
{
Assert.Equal("file", evidence.Kind);
Assert.Equal("/bin/busybox", evidence.Value);
Assert.Equal("abc123", evidence.Source);
},
evidence =>
{
Assert.Equal("config-file", evidence.Kind);
Assert.Equal("/etc/profile", evidence.Value);
Assert.Null(evidence.Source);
});
Assert.Equal(new[] { "musl>=1.2.5-r0", "ssl_client" }, component.Dependencies);
Assert.False(component.Usage.UsedByEntrypoint);
Assert.NotNull(component.Metadata);
Assert.Equal(new[] { "GPL-2.0-only" }, component.Metadata!.Licenses);
Assert.Contains("stellaops.os.analyzer", component.Metadata.Properties!.Keys);
Assert.Equal("apk", component.Metadata.Properties!["stellaops.os.analyzer"]);
Assert.Equal("https://busybox.net/", component.Metadata.Properties!["vendor.homepage"]);
}
}
using System.Collections.Generic;
using System.Collections.Immutable;
using StellaOps.Scanner.Analyzers.OS.Mapping;
using StellaOps.Scanner.Core.Contracts;
using Xunit;
namespace StellaOps.Scanner.Analyzers.OS.Tests.Mapping;
public class OsComponentMapperTests
{
[Fact]
public void ToLayerFragments_ProducesDeterministicComponents()
{
var package = new OSPackageRecord(
analyzerId: "apk",
packageUrl: "pkg:alpine/busybox@1.37.0-r0?arch=x86_64",
name: "busybox",
version: "1.37.0",
architecture: "x86_64",
evidenceSource: PackageEvidenceSource.ApkDatabase,
release: "r0",
sourcePackage: "busybox",
license: "GPL-2.0-only",
depends: new[] { "musl>=1.2.5-r0", "ssl_client" },
files: new[]
{
new OSPackageFileEvidence("/bin/busybox", sha256: "abc123", isConfigFile: false),
new OSPackageFileEvidence("/etc/profile", isConfigFile: true, digests: new Dictionary<string, string> { ["md5"] = "deadbeef" }),
},
vendorMetadata: new Dictionary<string, string?>
{
["homepage"] = "https://busybox.net/",
});
var result = new OSPackageAnalyzerResult(
analyzerId: "apk",
packages: ImmutableArray.Create(package),
telemetry: new OSAnalyzerTelemetry(System.TimeSpan.Zero, 1, 2));
var fragments = OsComponentMapper.ToLayerFragments(new[] { result });
Assert.Single(fragments);
var fragment = fragments[0];
Assert.StartsWith("sha256:", fragment.LayerDigest);
Assert.Single(fragment.Components);
var component = fragment.Components[0];
Assert.Equal(fragment.LayerDigest, component.LayerDigest);
Assert.Equal("pkg:alpine/busybox@1.37.0-r0?arch=x86_64", component.Identity.Key);
Assert.Equal("busybox", component.Identity.Name);
Assert.Equal("1.37.0", component.Identity.Version);
Assert.Equal("pkg:alpine/busybox@1.37.0-r0?arch=x86_64", component.Identity.Purl);
Assert.Equal("os-package", component.Identity.ComponentType);
Assert.Equal("busybox", component.Identity.Group);
Assert.Collection(component.Evidence,
evidence =>
{
Assert.Equal("file", evidence.Kind);
Assert.Equal("/bin/busybox", evidence.Value);
Assert.Equal("abc123", evidence.Source);
},
evidence =>
{
Assert.Equal("config-file", evidence.Kind);
Assert.Equal("/etc/profile", evidence.Value);
Assert.Equal("deadbeef", evidence.Source);
});
Assert.Equal(new[] { "musl>=1.2.5-r0", "ssl_client" }, component.Dependencies);
Assert.False(component.Usage.UsedByEntrypoint);
Assert.NotNull(component.Metadata);
Assert.Equal(new[] { "GPL-2.0-only" }, component.Metadata!.Licenses);
Assert.Contains("stellaops.os.analyzer", component.Metadata.Properties!.Keys);
Assert.Equal("apk", component.Metadata.Properties!["stellaops.os.analyzer"]);
Assert.Equal("https://busybox.net/", component.Metadata.Properties!["vendor.homepage"]);
}
}

View File

@@ -1,137 +1,137 @@
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
{
[Fact]
public async Task ApkAnalyzerMatchesGolden()
{
using var fixture = FixtureManager.UseFixture("apk", out var rootPath);
var analyzer = new ApkPackageAnalyzer(NullLogger<ApkPackageAnalyzer>.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"));
}
[Fact]
public async Task DpkgAnalyzerMatchesGolden()
{
using var fixture = FixtureManager.UseFixture("dpkg", out var rootPath);
var analyzer = new DpkgPackageAnalyzer(NullLogger<DpkgPackageAnalyzer>.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"));
}
[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<string, string> { ["sha256"] = "abc123" }),
new RpmFileEntry("/etc/pki/tls/openssl.cnf", true, new Dictionary<string, string> { ["md5"] = "c0ffee" })
},
changeLogs: new[] { "Resolves: CVE-2025-1234" },
metadata: new Dictionary<string, string?> { ["summary"] = "TLS toolkit" })
};
var reader = new StubRpmDatabaseReader(headers);
var analyzer = new RpmPackageAnalyzer(
NullLogger<RpmPackageAnalyzer>.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<string, string>
{
[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<string> provides,
IReadOnlyList<string> requires,
IReadOnlyList<RpmFileEntry> files,
IReadOnlyList<string> changeLogs,
IReadOnlyDictionary<string, string?> 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<RpmHeader> _headers;
public StubRpmDatabaseReader(IReadOnlyList<RpmHeader> headers)
{
_headers = headers;
}
public IReadOnlyList<RpmHeader> ReadHeaders(string rootPath, CancellationToken cancellationToken)
=> _headers;
}
}
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
{
[Fact]
public async Task ApkAnalyzerMatchesGolden()
{
using var fixture = FixtureManager.UseFixture("apk", out var rootPath);
var analyzer = new ApkPackageAnalyzer(NullLogger<ApkPackageAnalyzer>.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"));
}
[Fact]
public async Task DpkgAnalyzerMatchesGolden()
{
using var fixture = FixtureManager.UseFixture("dpkg", out var rootPath);
var analyzer = new DpkgPackageAnalyzer(NullLogger<DpkgPackageAnalyzer>.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"));
}
[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<string, string> { ["sha256"] = "abc123" }),
new RpmFileEntry("/etc/pki/tls/openssl.cnf", true, new Dictionary<string, string> { ["md5"] = "c0ffee" })
},
changeLogs: new[] { "Resolves: CVE-2025-1234" },
metadata: new Dictionary<string, string?> { ["summary"] = "TLS toolkit" })
};
var reader = new StubRpmDatabaseReader(headers);
var analyzer = new RpmPackageAnalyzer(
NullLogger<RpmPackageAnalyzer>.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<string, string>
{
[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<string> provides,
IReadOnlyList<string> requires,
IReadOnlyList<RpmFileEntry> files,
IReadOnlyList<string> changeLogs,
IReadOnlyDictionary<string, string?> 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<RpmHeader> _headers;
public StubRpmDatabaseReader(IReadOnlyList<RpmHeader> headers)
{
_headers = headers;
}
public IReadOnlyList<RpmHeader> ReadHeaders(string rootPath, CancellationToken cancellationToken)
=> _headers;
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Buffers.Binary;
using System.IO;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Scanner.Analyzers.OS.Rpm;
using Xunit;
@@ -9,6 +10,42 @@ namespace StellaOps.Scanner.Analyzers.OS.Tests.Rpm;
public sealed class RpmDatabaseReaderTests
{
[Theory]
[InlineData("hdr")]
[InlineData("header")]
[InlineData("headerBlob")]
public void ReadsHeaders_FromSqlite_WhenHeaderBlobColumnPresent(string headerColumnName)
{
var root = Directory.CreateTempSubdirectory("rpmdb-sqlite");
try
{
var rpmPath = Path.Combine(root.FullName, "var", "lib", "rpm");
Directory.CreateDirectory(rpmPath);
var sqlitePath = Path.Combine(rpmPath, "rpmdb.sqlite");
CreateSqliteRpmdb(sqlitePath, headerColumnName);
var reader = new RpmDatabaseReader(NullLogger.Instance);
var headers = reader.ReadHeaders(root.FullName, CancellationToken.None);
Assert.Single(headers);
var header = headers[0];
Assert.Equal("sqlite-pkg", header.Name);
Assert.Equal("2.0.0", header.Version);
Assert.Equal("aarch64", header.Architecture);
}
finally
{
try
{
root.Delete(recursive: true);
}
catch
{
}
}
}
[Fact]
public void FallsBackToLegacyPackages_WhenSqliteMissing()
{
@@ -42,6 +79,36 @@ public sealed class RpmDatabaseReaderTests
}
}
private static void CreateSqliteRpmdb(string sqlitePath, string headerColumnName)
{
var connectionString = new SqliteConnectionStringBuilder
{
DataSource = sqlitePath,
Mode = SqliteOpenMode.ReadWriteCreate,
}.ToString();
using var connection = new SqliteConnection(connectionString);
connection.Open();
using var create = connection.CreateCommand();
create.CommandText = $@"CREATE TABLE Packages (
pkgKey INTEGER PRIMARY KEY,
pkgId BLOB,
""{headerColumnName}"" BLOB
);";
create.ExecuteNonQuery();
var header = CreateRpmHeader("sqlite-pkg", "2.0.0", "aarch64");
using var insert = connection.CreateCommand();
insert.CommandText = $@"INSERT INTO Packages (pkgKey, pkgId, ""{headerColumnName}"")
VALUES ($key, $pkgId, $hdr);";
insert.Parameters.AddWithValue("$key", 1);
insert.Parameters.AddWithValue("$pkgId", new byte[] { 0x01, 0x02, 0x03, 0x04 }); // not an RPM header
insert.Parameters.AddWithValue("$hdr", header);
insert.ExecuteNonQuery();
}
private static byte[] CreateLegacyPackagesFile()
{
const int pageSize = 4096;

View File

@@ -1,75 +1,75 @@
using System;
using System.IO;
namespace StellaOps.Scanner.Analyzers.OS.Tests.TestUtilities;
internal static class FixtureManager
{
public static IDisposable UseFixture(string name, out string rootPath)
{
var basePath = Path.Combine(AppContext.BaseDirectory, "Fixtures", name);
if (!Directory.Exists(basePath))
{
throw new DirectoryNotFoundException($"Fixture '{name}' was not found at '{basePath}'.");
}
var tempRoot = Path.Combine(Path.GetTempPath(), "stellaops-os-fixture", name, Guid.NewGuid().ToString("n"));
CopyDirectory(basePath, tempRoot);
rootPath = tempRoot;
return new Disposable(() => DeleteDirectory(tempRoot));
}
public static string GetGoldenPath(string name)
=> Path.Combine(AppContext.BaseDirectory, "Fixtures", "goldens", name);
private static void CopyDirectory(string source, string destination)
{
Directory.CreateDirectory(destination);
foreach (var file in Directory.GetFiles(source, "*", SearchOption.AllDirectories))
{
var relative = Path.GetRelativePath(source, file);
var target = Path.Combine(destination, relative);
Directory.CreateDirectory(Path.GetDirectoryName(target)!);
File.Copy(file, target);
}
}
private static void DeleteDirectory(string path)
{
if (!Directory.Exists(path))
{
return;
}
try
{
Directory.Delete(path, recursive: true);
}
catch
{
// best-effort cleanup
}
}
private sealed class Disposable : IDisposable
{
private readonly Action _dispose;
private bool _disposed;
public Disposable(Action dispose)
{
_dispose = dispose;
}
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
_dispose();
}
}
}
using System;
using System.IO;
namespace StellaOps.Scanner.Analyzers.OS.Tests.TestUtilities;
internal static class FixtureManager
{
public static IDisposable UseFixture(string name, out string rootPath)
{
var basePath = Path.Combine(AppContext.BaseDirectory, "Fixtures", name);
if (!Directory.Exists(basePath))
{
throw new DirectoryNotFoundException($"Fixture '{name}' was not found at '{basePath}'.");
}
var tempRoot = Path.Combine(Path.GetTempPath(), "stellaops-os-fixture", name, Guid.NewGuid().ToString("n"));
CopyDirectory(basePath, tempRoot);
rootPath = tempRoot;
return new Disposable(() => DeleteDirectory(tempRoot));
}
public static string GetGoldenPath(string name)
=> Path.Combine(AppContext.BaseDirectory, "Fixtures", "goldens", name);
private static void CopyDirectory(string source, string destination)
{
Directory.CreateDirectory(destination);
foreach (var file in Directory.GetFiles(source, "*", SearchOption.AllDirectories))
{
var relative = Path.GetRelativePath(source, file);
var target = Path.Combine(destination, relative);
Directory.CreateDirectory(Path.GetDirectoryName(target)!);
File.Copy(file, target);
}
}
private static void DeleteDirectory(string path)
{
if (!Directory.Exists(path))
{
return;
}
try
{
Directory.Delete(path, recursive: true);
}
catch
{
// best-effort cleanup
}
}
private sealed class Disposable : IDisposable
{
private readonly Action _dispose;
private bool _disposed;
public Disposable(Action dispose)
{
_dispose = dispose;
}
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
_dispose();
}
}
}

View File

@@ -1,41 +1,41 @@
using System;
using System.IO;
using Xunit;
namespace StellaOps.Scanner.Analyzers.OS.Tests.TestUtilities;
internal static class GoldenAssert
{
private const string UpdateEnvironmentVariable = "UPDATE_OS_ANALYZER_FIXTURES";
public static void MatchSnapshot(string snapshot, string goldenPath)
{
var directory = Path.GetDirectoryName(goldenPath);
if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
snapshot = Normalize(snapshot);
if (!File.Exists(goldenPath))
{
File.WriteAllText(goldenPath, snapshot);
return;
}
if (ShouldUpdate())
{
File.WriteAllText(goldenPath, snapshot);
}
var expected = Normalize(File.ReadAllText(goldenPath));
Assert.Equal(expected.TrimEnd(), snapshot.TrimEnd());
}
private static bool ShouldUpdate()
=> string.Equals(Environment.GetEnvironmentVariable(UpdateEnvironmentVariable), "1", StringComparison.OrdinalIgnoreCase);
private static string Normalize(string value)
=> value.Replace("\r\n", "\n");
}
using System;
using System.IO;
using Xunit;
namespace StellaOps.Scanner.Analyzers.OS.Tests.TestUtilities;
internal static class GoldenAssert
{
private const string UpdateEnvironmentVariable = "UPDATE_OS_ANALYZER_FIXTURES";
public static void MatchSnapshot(string snapshot, string goldenPath)
{
var directory = Path.GetDirectoryName(goldenPath);
if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
snapshot = Normalize(snapshot);
if (!File.Exists(goldenPath))
{
File.WriteAllText(goldenPath, snapshot);
return;
}
if (ShouldUpdate())
{
File.WriteAllText(goldenPath, snapshot);
}
var expected = Normalize(File.ReadAllText(goldenPath));
Assert.Equal(expected.TrimEnd(), snapshot.TrimEnd());
}
private static bool ShouldUpdate()
=> string.Equals(Environment.GetEnvironmentVariable(UpdateEnvironmentVariable), "1", StringComparison.OrdinalIgnoreCase);
private static string Normalize(string value)
=> value.Replace("\r\n", "\n");
}

View File

@@ -1,106 +1,106 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using StellaOps.Scanner.Analyzers.OS;
namespace StellaOps.Scanner.Analyzers.OS.Tests.TestUtilities;
internal static class SnapshotSerializer
{
private static readonly JsonSerializerOptions Options = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true,
Converters =
{
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)
}
};
public static string Serialize(IEnumerable<OSPackageAnalyzerResult> results)
{
var ordered = results
.OrderBy(r => r.AnalyzerId, StringComparer.OrdinalIgnoreCase)
.Select(result => new AnalyzerSnapshot
{
AnalyzerId = result.AnalyzerId,
PackageCount = result.Telemetry.PackageCount,
FileEvidenceCount = result.Telemetry.FileEvidenceCount,
DurationMilliseconds = 0,
Warnings = result.Warnings.Select(w => new WarningSnapshot(w.Code, w.Message)).ToArray(),
Packages = result.Packages
.OrderBy(p => p, Comparer<OSPackageRecord>.Default)
.Select(p => new PackageSnapshot
{
PackageUrl = p.PackageUrl,
Name = p.Name,
Version = p.Version,
Architecture = p.Architecture,
Epoch = p.Epoch,
Release = p.Release,
SourcePackage = p.SourcePackage,
License = p.License,
EvidenceSource = p.EvidenceSource.ToString(),
CveHints = p.CveHints,
Provides = p.Provides,
Depends = p.Depends,
Files = p.Files.Select(f => new FileSnapshot
{
Path = f.Path,
LayerDigest = f.LayerDigest,
Sha256 = f.Sha256,
SizeBytes = f.SizeBytes,
IsConfigFile = f.IsConfigFile,
Digests = f.Digests.OrderBy(kv => kv.Key, StringComparer.OrdinalIgnoreCase).ToDictionary(kv => kv.Key, kv => kv.Value, StringComparer.OrdinalIgnoreCase)
}).ToArray(),
VendorMetadata = p.VendorMetadata.OrderBy(kv => kv.Key, StringComparer.Ordinal).ToDictionary(kv => kv.Key, kv => kv.Value, StringComparer.Ordinal)
}).ToArray()
})
.ToArray();
return JsonSerializer.Serialize(ordered, Options);
}
private sealed record AnalyzerSnapshot
{
public string AnalyzerId { get; init; } = string.Empty;
public double DurationMilliseconds { get; init; }
public int PackageCount { get; init; }
public int FileEvidenceCount { get; init; }
public IReadOnlyList<WarningSnapshot> Warnings { get; init; } = Array.Empty<WarningSnapshot>();
public IReadOnlyList<PackageSnapshot> Packages { get; init; } = Array.Empty<PackageSnapshot>();
}
private sealed record WarningSnapshot(string Code, string Message);
private sealed record PackageSnapshot
{
public string PackageUrl { get; init; } = string.Empty;
public string Name { get; init; } = string.Empty;
public string Version { get; init; } = string.Empty;
public string Architecture { get; init; } = string.Empty;
public string? Epoch { get; init; }
public string? Release { get; init; }
public string? SourcePackage { get; init; }
public string? License { get; init; }
public string EvidenceSource { get; init; } = string.Empty;
public IReadOnlyList<string> CveHints { get; init; } = Array.Empty<string>();
public IReadOnlyList<string> Provides { get; init; } = Array.Empty<string>();
public IReadOnlyList<string> Depends { get; init; } = Array.Empty<string>();
public IReadOnlyList<FileSnapshot> Files { get; init; } = Array.Empty<FileSnapshot>();
public IReadOnlyDictionary<string, string?> VendorMetadata { get; init; } = new Dictionary<string, string?>();
}
private sealed record FileSnapshot
{
public string Path { get; init; } = string.Empty;
public string? LayerDigest { get; init; }
public string? Sha256 { get; init; }
public long? SizeBytes { get; init; }
public bool? IsConfigFile { get; init; }
public IReadOnlyDictionary<string, string> Digests { get; init; } = new Dictionary<string, string>();
}
}
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using StellaOps.Scanner.Analyzers.OS;
namespace StellaOps.Scanner.Analyzers.OS.Tests.TestUtilities;
internal static class SnapshotSerializer
{
private static readonly JsonSerializerOptions Options = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true,
Converters =
{
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)
}
};
public static string Serialize(IEnumerable<OSPackageAnalyzerResult> results)
{
var ordered = results
.OrderBy(r => r.AnalyzerId, StringComparer.OrdinalIgnoreCase)
.Select(result => new AnalyzerSnapshot
{
AnalyzerId = result.AnalyzerId,
PackageCount = result.Telemetry.PackageCount,
FileEvidenceCount = result.Telemetry.FileEvidenceCount,
DurationMilliseconds = 0,
Warnings = result.Warnings.Select(w => new WarningSnapshot(w.Code, w.Message)).ToArray(),
Packages = result.Packages
.OrderBy(p => p, Comparer<OSPackageRecord>.Default)
.Select(p => new PackageSnapshot
{
PackageUrl = p.PackageUrl,
Name = p.Name,
Version = p.Version,
Architecture = p.Architecture,
Epoch = p.Epoch,
Release = p.Release,
SourcePackage = p.SourcePackage,
License = p.License,
EvidenceSource = p.EvidenceSource.ToString(),
CveHints = p.CveHints,
Provides = p.Provides,
Depends = p.Depends,
Files = p.Files.Select(f => new FileSnapshot
{
Path = f.Path,
LayerDigest = f.LayerDigest,
Sha256 = f.Sha256,
SizeBytes = f.SizeBytes,
IsConfigFile = f.IsConfigFile,
Digests = f.Digests.OrderBy(kv => kv.Key, StringComparer.OrdinalIgnoreCase).ToDictionary(kv => kv.Key, kv => kv.Value, StringComparer.OrdinalIgnoreCase)
}).ToArray(),
VendorMetadata = p.VendorMetadata.OrderBy(kv => kv.Key, StringComparer.Ordinal).ToDictionary(kv => kv.Key, kv => kv.Value, StringComparer.Ordinal)
}).ToArray()
})
.ToArray();
return JsonSerializer.Serialize(ordered, Options);
}
private sealed record AnalyzerSnapshot
{
public string AnalyzerId { get; init; } = string.Empty;
public double DurationMilliseconds { get; init; }
public int PackageCount { get; init; }
public int FileEvidenceCount { get; init; }
public IReadOnlyList<WarningSnapshot> Warnings { get; init; } = Array.Empty<WarningSnapshot>();
public IReadOnlyList<PackageSnapshot> Packages { get; init; } = Array.Empty<PackageSnapshot>();
}
private sealed record WarningSnapshot(string Code, string Message);
private sealed record PackageSnapshot
{
public string PackageUrl { get; init; } = string.Empty;
public string Name { get; init; } = string.Empty;
public string Version { get; init; } = string.Empty;
public string Architecture { get; init; } = string.Empty;
public string? Epoch { get; init; }
public string? Release { get; init; }
public string? SourcePackage { get; init; }
public string? License { get; init; }
public string EvidenceSource { get; init; } = string.Empty;
public IReadOnlyList<string> CveHints { get; init; } = Array.Empty<string>();
public IReadOnlyList<string> Provides { get; init; } = Array.Empty<string>();
public IReadOnlyList<string> Depends { get; init; } = Array.Empty<string>();
public IReadOnlyList<FileSnapshot> Files { get; init; } = Array.Empty<FileSnapshot>();
public IReadOnlyDictionary<string, string?> VendorMetadata { get; init; } = new Dictionary<string, string?>();
}
private sealed record FileSnapshot
{
public string Path { get; init; } = string.Empty;
public string? LayerDigest { get; init; }
public string? Sha256 { get; init; }
public long? SizeBytes { get; init; }
public bool? IsConfigFile { get; init; }
public IReadOnlyDictionary<string, string> Digests { get; init; } = new Dictionary<string, string>();
}
}