Restructure solution layout by module
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
C:Q1
|
||||
P:busybox
|
||||
V:1.37.0-r0
|
||||
A:x86_64
|
||||
S:4096
|
||||
I:8192
|
||||
T:BusyBox utility set
|
||||
U:https://busybox.net/
|
||||
L:GPL-2.0-only
|
||||
o:busybox
|
||||
m:Stella Ops <ops@stella-ops.org>
|
||||
t:1729286400
|
||||
c:deadbeef12345678
|
||||
D:musl>=1.2.5-r0 ssl_client
|
||||
p:/bin/sh
|
||||
F:bin
|
||||
R:busybox
|
||||
Z:0f1e2d3c4b5a6978ffeeddccbbaa9988
|
||||
F:etc
|
||||
R:profile
|
||||
Z:11223344556677889900aabbccddeeff
|
||||
a:0:0:0644
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
/etc/bash.bashrc abcdef1234567890
|
||||
@@ -0,0 +1,3 @@
|
||||
/bin/bash
|
||||
/etc/bash.bashrc
|
||||
/usr/share/doc/bash/changelog.Debian.gz
|
||||
@@ -0,0 +1,2 @@
|
||||
0123456789abcdef0123456789abcdef /bin/bash
|
||||
abcdef1234567890abcdef1234567890 /etc/bash.bashrc
|
||||
@@ -0,0 +1,15 @@
|
||||
Package: bash
|
||||
Status: install ok installed
|
||||
Priority: important
|
||||
Section: shells
|
||||
Installed-Size: 1024
|
||||
Maintainer: Debian Developers <debian-devel@lists.debian.org>
|
||||
Architecture: amd64
|
||||
Version: 5.2.21-2
|
||||
Source: bash (5.2.21-2)
|
||||
Homepage: https://www.gnu.org/software/bash/
|
||||
Description: GNU Bourne Again Shell
|
||||
This is the GNU Bourne Again Shell.
|
||||
Conffiles:
|
||||
/etc/bash.bashrc abcdef1234567890
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
[
|
||||
{
|
||||
"analyzerId": "apk",
|
||||
"durationMilliseconds": 0,
|
||||
"packageCount": 1,
|
||||
"fileEvidenceCount": 4,
|
||||
"warnings": [],
|
||||
"packages": [
|
||||
{
|
||||
"packageUrl": "pkg:alpine/busybox@1.37.0-r0?arch=x86_64",
|
||||
"name": "busybox",
|
||||
"version": "1.37.0",
|
||||
"architecture": "x86_64",
|
||||
"epoch": null,
|
||||
"release": "r0",
|
||||
"sourcePackage": "busybox",
|
||||
"license": "GPL-2.0-only",
|
||||
"evidenceSource": "ApkDatabase",
|
||||
"cveHints": [],
|
||||
"provides": [
|
||||
"/bin/sh"
|
||||
],
|
||||
"depends": [
|
||||
"musl\u003E=1.2.5-r0",
|
||||
"ssl_client"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "/bin/",
|
||||
"layerDigest": null,
|
||||
"sha256": null,
|
||||
"sizeBytes": null,
|
||||
"isConfigFile": false,
|
||||
"digests": {}
|
||||
},
|
||||
{
|
||||
"path": "/bin/busybox",
|
||||
"layerDigest": null,
|
||||
"sha256": null,
|
||||
"sizeBytes": null,
|
||||
"isConfigFile": false,
|
||||
"digests": {}
|
||||
},
|
||||
{
|
||||
"path": "/etc/",
|
||||
"layerDigest": null,
|
||||
"sha256": null,
|
||||
"sizeBytes": null,
|
||||
"isConfigFile": false,
|
||||
"digests": {}
|
||||
},
|
||||
{
|
||||
"path": "/etc/profile",
|
||||
"layerDigest": null,
|
||||
"sha256": "0f1e2d3c4b5a6978ffeeddccbbaa9988",
|
||||
"sizeBytes": null,
|
||||
"isConfigFile": false,
|
||||
"digests": {
|
||||
"sha256": "0f1e2d3c4b5a6978ffeeddccbbaa9988"
|
||||
}
|
||||
}
|
||||
],
|
||||
"vendorMetadata": {
|
||||
"buildTime": "1729286400",
|
||||
"checksum": "deadbeef12345678",
|
||||
"description": "BusyBox utility set",
|
||||
"homepage": "https://busybox.net/",
|
||||
"maintainer": "Stella Ops \u003Cops@stella-ops.org\u003E",
|
||||
"origin": "busybox"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,64 @@
|
||||
[
|
||||
{
|
||||
"analyzerId": "dpkg",
|
||||
"durationMilliseconds": 0,
|
||||
"packageCount": 1,
|
||||
"fileEvidenceCount": 3,
|
||||
"warnings": [],
|
||||
"packages": [
|
||||
{
|
||||
"packageUrl": "pkg:deb/debian/bash@5.2.21-2?arch=amd64",
|
||||
"name": "bash",
|
||||
"version": "5.2.21",
|
||||
"architecture": "amd64",
|
||||
"epoch": null,
|
||||
"release": "2",
|
||||
"sourcePackage": "bash",
|
||||
"license": null,
|
||||
"evidenceSource": "DpkgStatus",
|
||||
"cveHints": [],
|
||||
"provides": [],
|
||||
"depends": [],
|
||||
"files": [
|
||||
{
|
||||
"path": "/bin/bash",
|
||||
"layerDigest": null,
|
||||
"sha256": null,
|
||||
"sizeBytes": null,
|
||||
"isConfigFile": false,
|
||||
"digests": {
|
||||
"md5": "0123456789abcdef0123456789abcdef"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/etc/bash.bashrc",
|
||||
"layerDigest": null,
|
||||
"sha256": null,
|
||||
"sizeBytes": null,
|
||||
"isConfigFile": true,
|
||||
"digests": {
|
||||
"md5": "abcdef1234567890abcdef1234567890"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/usr/share/doc/bash/changelog.Debian.gz",
|
||||
"layerDigest": null,
|
||||
"sha256": null,
|
||||
"sizeBytes": null,
|
||||
"isConfigFile": false,
|
||||
"digests": {}
|
||||
}
|
||||
],
|
||||
"vendorMetadata": {
|
||||
"dpkg:Installed-Size": "1024",
|
||||
"homepage": "https://www.gnu.org/software/bash/",
|
||||
"maintainer": "Debian Developers \u003Cdebian-devel@lists.debian.org\u003E",
|
||||
"origin": null,
|
||||
"priority": "important",
|
||||
"section": "shells",
|
||||
"source": "bash (5.2.21-2)"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,64 @@
|
||||
[
|
||||
{
|
||||
"analyzerId": "rpm",
|
||||
"durationMilliseconds": 0,
|
||||
"packageCount": 1,
|
||||
"fileEvidenceCount": 2,
|
||||
"warnings": [],
|
||||
"packages": [
|
||||
{
|
||||
"packageUrl": "pkg:rpm/openssl-libs@1:3.2.1-8.el9?arch=x86_64",
|
||||
"name": "openssl-libs",
|
||||
"version": "3.2.1",
|
||||
"architecture": "x86_64",
|
||||
"epoch": "1",
|
||||
"release": "8.el9",
|
||||
"sourcePackage": "openssl-3.2.1-8.el9.src.rpm",
|
||||
"license": "OpenSSL",
|
||||
"evidenceSource": "RpmDatabase",
|
||||
"cveHints": [
|
||||
"CVE-2025-1234"
|
||||
],
|
||||
"provides": [
|
||||
"libcrypto.so.3()(64bit)",
|
||||
"openssl-libs"
|
||||
],
|
||||
"depends": [
|
||||
"glibc(x86-64) \u003E= 2.34"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "/etc/pki/tls/openssl.cnf",
|
||||
"layerDigest": null,
|
||||
"sha256": null,
|
||||
"sizeBytes": null,
|
||||
"isConfigFile": true,
|
||||
"digests": {
|
||||
"md5": "c0ffee"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/usr/lib64/libcrypto.so.3",
|
||||
"layerDigest": null,
|
||||
"sha256": "abc123",
|
||||
"sizeBytes": null,
|
||||
"isConfigFile": false,
|
||||
"digests": {
|
||||
"sha256": "abc123"
|
||||
}
|
||||
}
|
||||
],
|
||||
"vendorMetadata": {
|
||||
"buildTime": null,
|
||||
"description": null,
|
||||
"installTime": null,
|
||||
"rpm:summary": "TLS toolkit",
|
||||
"sourceRpm": "openssl-3.2.1-8.el9.src.rpm",
|
||||
"summary": "TLS toolkit",
|
||||
"url": null,
|
||||
"vendor": null
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +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"]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<UseConcelierTestInfra>false</UseConcelierTestInfra>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0-rc.2.25502.107" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Analyzers.OS/StellaOps.Scanner.Analyzers.OS.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Analyzers.OS.Apk/StellaOps.Scanner.Analyzers.OS.Apk.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Analyzers.OS.Dpkg/StellaOps.Scanner.Analyzers.OS.Dpkg.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Analyzers.OS.Rpm/StellaOps.Scanner.Analyzers.OS.Rpm.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +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");
|
||||
}
|
||||
@@ -0,0 +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>();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user