using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Text.Json; using System.Security.Cryptography; using StellaOps.Feedser.Exporter.TrivyDb; namespace StellaOps.Feedser.Exporter.TrivyDb.Tests; public sealed class TrivyDbOciWriterTests : IDisposable { private readonly string _root; public TrivyDbOciWriterTests() { _root = Directory.CreateTempSubdirectory("feedser-trivy-oci-tests").FullName; } [Fact] public async Task WritesOciLayoutWithManifestIndex() { var metadata = Encoding.UTF8.GetBytes("{\"generatedAt\":\"2024-08-01T00:00:00Z\",\"schema\":1}"); var archive = Enumerable.Range(0, 128).Select(static b => (byte)b).ToArray(); var generatedAt = DateTimeOffset.Parse("2024-08-01T00:00:00Z"); var archivePath = Path.Combine(_root, "db.bin"); File.WriteAllBytes(archivePath, archive); var archiveDigest = ComputeDigest(archive); var request = new TrivyDbPackageRequest(metadata, archivePath, archiveDigest, archive.LongLength, generatedAt, "2024.08.01"); var builder = new TrivyDbPackageBuilder(); var package = builder.BuildPackage(request); var writer = new TrivyDbOciWriter(); var result = await writer.WriteAsync(package, Path.Combine(_root, "oci"), "feedser:v2024.08.01", CancellationToken.None); Assert.Equal(package.Manifest.Layers[0].Digest, package.Config.DatabaseDigest); Assert.NotEmpty(result.BlobDigests); Assert.Contains(result.ManifestDigest, result.BlobDigests); var layoutPath = Path.Combine(result.RootDirectory, "oci-layout"); Assert.True(File.Exists(layoutPath)); var layoutJson = await File.ReadAllTextAsync(layoutPath, CancellationToken.None); Assert.Contains("\"imageLayoutVersion\":\"1.0.0\"", layoutJson, StringComparison.Ordinal); var metadataPath = Path.Combine(result.RootDirectory, "metadata.json"); Assert.True(File.Exists(metadataPath)); var roundTripMetadata = await File.ReadAllBytesAsync(metadataPath, CancellationToken.None); Assert.Equal(metadata, roundTripMetadata); var indexPath = Path.Combine(result.RootDirectory, "index.json"); Assert.True(File.Exists(indexPath)); using var indexDocument = JsonDocument.Parse(await File.ReadAllBytesAsync(indexPath, CancellationToken.None)); var manifestElement = indexDocument.RootElement.GetProperty("manifests")[0]; Assert.Equal(result.ManifestDigest, manifestElement.GetProperty("digest").GetString()); Assert.Equal(TrivyDbMediaTypes.OciManifest, manifestElement.GetProperty("mediaType").GetString()); Assert.Equal("feedser:v2024.08.01", manifestElement.GetProperty("annotations").GetProperty("org.opencontainers.image.ref.name").GetString()); var manifestPath = Path.Combine(result.RootDirectory, "blobs", "sha256", result.ManifestDigest.Split(':')[1]); var manifestBytes = await File.ReadAllBytesAsync(manifestPath, CancellationToken.None); using var manifestDocument = JsonDocument.Parse(manifestBytes); var configDescriptor = manifestDocument.RootElement.GetProperty("config"); Assert.Equal(package.Manifest.Config.Digest, configDescriptor.GetProperty("digest").GetString()); Assert.Equal(package.Manifest.Config.MediaType, configDescriptor.GetProperty("mediaType").GetString()); var layer = manifestDocument.RootElement.GetProperty("layers")[0]; Assert.Equal(package.Manifest.Layers[0].Digest, layer.GetProperty("digest").GetString()); Assert.Equal(package.Manifest.Layers[0].MediaType, layer.GetProperty("mediaType").GetString()); foreach (var digest in package.Blobs.Keys) { var blobPath = Path.Combine(result.RootDirectory, "blobs", "sha256", digest.Split(':')[1]); Assert.True(File.Exists(blobPath)); } } [Fact] public async Task ThrowsOnUnsupportedDigest() { var package = new TrivyDbPackage( new OciManifest(2, TrivyDbMediaTypes.OciManifest, new OciDescriptor(TrivyDbMediaTypes.TrivyConfig, "sha256:abcd", 4), Array.Empty()), new TrivyConfigDocument(TrivyDbMediaTypes.TrivyConfig, DateTimeOffset.UtcNow, "1", "sha256:abcd", 4), new Dictionary { ["md5:deadbeef"] = TrivyDbBlob.FromBytes(new byte[] { 1, 2, 3, 4 }), }, new byte[] { 123 }); var writer = new TrivyDbOciWriter(); await Assert.ThrowsAsync(() => writer.WriteAsync(package, Path.Combine(_root, "invalid"), "feedser:bad", CancellationToken.None)); } public void Dispose() { try { if (Directory.Exists(_root)) { Directory.Delete(_root, recursive: true); } } catch { // ignore cleanup issues } } private static string ComputeDigest(byte[] payload) { var hash = SHA256.HashData(payload); return "sha256:" + Convert.ToHexString(hash).ToLowerInvariant(); } }