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
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user