Implement MongoDB-based storage for Pack Run approval, artifact, log, and state management
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

- Added MongoPackRunApprovalStore for managing approval states with MongoDB.
- Introduced MongoPackRunArtifactUploader for uploading and storing artifacts.
- Created MongoPackRunLogStore to handle logging of pack run events.
- Developed MongoPackRunStateStore for persisting and retrieving pack run states.
- Implemented unit tests for MongoDB stores to ensure correct functionality.
- Added MongoTaskRunnerTestContext for setting up MongoDB test environment.
- Enhanced PackRunStateFactory to correctly initialize state with gate reasons.
This commit is contained in:
master
2025-11-07 10:01:35 +02:00
parent e5ffcd6535
commit a1ce3f74fa
122 changed files with 8730 additions and 914 deletions

View File

@@ -0,0 +1,122 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using StellaOps.Scanner.Sbomer.BuildXPlugin.Descriptor;
using StellaOps.Scanner.Sbomer.BuildXPlugin.Manifest;
using StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.TestUtilities;
using StellaOps.Scanner.Surface.FS;
using Xunit;
namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.Descriptor;
public sealed class DescriptorCommandSurfaceTests
{
[Fact]
public async Task DescriptorCommand_PublishesSurfaceArtifacts()
{
await using var temp = new TempDirectory();
var casRoot = Path.Combine(temp.Path, "cas");
Directory.CreateDirectory(casRoot);
var sbomPath = Path.Combine(temp.Path, "sample.cdx.json");
await File.WriteAllTextAsync(sbomPath, "{\"bomFormat\":\"CycloneDX\",\"specVersion\":\"1.6\"}");
var layerFragmentsPath = Path.Combine(temp.Path, "layer-fragments.json");
await File.WriteAllTextAsync(layerFragmentsPath, "[]");
var entryTraceGraphPath = Path.Combine(temp.Path, "entrytrace-graph.json");
await File.WriteAllTextAsync(entryTraceGraphPath, "{\"nodes\":[],\"edges\":[]}");
var entryTraceNdjsonPath = Path.Combine(temp.Path, "entrytrace.ndjson");
await File.WriteAllTextAsync(entryTraceNdjsonPath, "{}\n{}");
var manifestOutputPath = Path.Combine(temp.Path, "out", "surface-manifest.json");
var repoRoot = TestPathHelper.FindRepositoryRoot();
var manifestDirectory = Path.Combine(repoRoot, "src", "Scanner", "StellaOps.Scanner.Sbomer.BuildXPlugin");
var pluginAssembly = typeof(BuildxPluginManifest).Assembly.Location;
var psi = new ProcessStartInfo("dotnet")
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
WorkingDirectory = repoRoot
};
psi.ArgumentList.Add(pluginAssembly);
psi.ArgumentList.Add("descriptor");
psi.ArgumentList.Add("--manifest");
psi.ArgumentList.Add(manifestDirectory);
psi.ArgumentList.Add("--cas");
psi.ArgumentList.Add(casRoot);
psi.ArgumentList.Add("--image");
psi.ArgumentList.Add("sha256:feedfacefeedfacefeedfacefeedfacefeedfacefeedfacefeedfacefeedface");
psi.ArgumentList.Add("--sbom");
psi.ArgumentList.Add(sbomPath);
psi.ArgumentList.Add("--sbom-name");
psi.ArgumentList.Add("sample.cdx.json");
psi.ArgumentList.Add("--surface-layer-fragments");
psi.ArgumentList.Add(layerFragmentsPath);
psi.ArgumentList.Add("--surface-entrytrace-graph");
psi.ArgumentList.Add(entryTraceGraphPath);
psi.ArgumentList.Add("--surface-entrytrace-ndjson");
psi.ArgumentList.Add(entryTraceNdjsonPath);
psi.ArgumentList.Add("--surface-cache-root");
psi.ArgumentList.Add(casRoot);
psi.ArgumentList.Add("--surface-tenant");
psi.ArgumentList.Add("test-tenant");
psi.ArgumentList.Add("--surface-manifest-output");
psi.ArgumentList.Add(manifestOutputPath);
var process = Process.Start(psi) ?? throw new InvalidOperationException("Failed to start BuildX plug-in process.");
var stdout = await process.StandardOutput.ReadToEndAsync();
var stderr = await process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync();
Assert.True(process.ExitCode == 0, $"Descriptor command failed.\nSTDOUT: {stdout}\nSTDERR: {stderr}");
var descriptor = JsonSerializer.Deserialize<DescriptorDocument>(stdout, new JsonSerializerOptions(JsonSerializerDefaults.Web));
Assert.NotNull(descriptor);
Assert.Equal("stellaops.buildx.descriptor.v1", descriptor!.Schema);
Assert.Equal("sha256:d07d06ae82e1789a5b505731f3ec3add106e23a55395213c9a881c7e816c695c", descriptor.Artifact.Digest);
Assert.Contains("surface manifest stored", stderr, StringComparison.OrdinalIgnoreCase);
Assert.True(File.Exists(manifestOutputPath));
var surfaceManifestPath = Directory.GetFiles(Path.Combine(casRoot, "scanner", "surface", "manifests"), "*.json", SearchOption.AllDirectories).Single();
var manifestDocument = JsonSerializer.Deserialize<SurfaceManifestDocument>(await File.ReadAllTextAsync(surfaceManifestPath), new JsonSerializerOptions(JsonSerializerDefaults.Web));
Assert.NotNull(manifestDocument);
Assert.Equal("test-tenant", manifestDocument!.Tenant);
Assert.Equal(3, manifestDocument.Artifacts.Count);
foreach (var artifact in manifestDocument.Artifacts)
{
Assert.StartsWith("cas://", artifact.Uri, StringComparison.Ordinal);
var localPath = ResolveLocalPath(artifact.Uri, casRoot);
Assert.True(File.Exists(localPath), $"Missing CAS object for {artifact.Uri}");
}
}
private static string ResolveLocalPath(string casUri, string casRoot)
{
const string prefix = "cas://";
if (!casUri.StartsWith(prefix, StringComparison.Ordinal))
{
throw new InvalidOperationException($"Unsupported CAS URI {casUri}.");
}
var slashIndex = casUri.IndexOf(/, prefix.Length);
if (slashIndex < 0)
{
throw new InvalidOperationException($"CAS URI {casUri} does not contain a bucket path.");
}
var relative = casUri[(slashIndex + 1)..];
var localPath = Path.Combine(casRoot, relative.Replace(/, Path.DirectorySeparatorChar));
return localPath;
}
}

View File

@@ -1,45 +1,45 @@
{
"schema": "stellaops.buildx.descriptor.v1",
"generatedAt": "2025-10-18T12:00:00\u002B00:00",
"generator": {
"name": "StellaOps.Scanner.Sbomer.BuildXPlugin",
"version": "1.2.3"
},
"subject": {
"mediaType": "application/vnd.oci.image.manifest.v1\u002Bjson",
"digest": "sha256:0123456789abcdef"
},
"artifact": {
"mediaType": "application/vnd.cyclonedx\u002Bjson",
"digest": "sha256:d07d06ae82e1789a5b505731f3ec3add106e23a55395213c9a881c7e816c695c",
"size": 45,
"annotations": {
"org.opencontainers.artifact.type": "application/vnd.stellaops.sbom.layer\u002Bjson",
"org.stellaops.scanner.version": "1.2.3",
"org.stellaops.sbom.kind": "inventory",
"org.stellaops.sbom.format": "cyclonedx-json",
"org.stellaops.provenance.status": "pending",
"org.stellaops.provenance.dsse.sha256": "sha256:1b364a6b888d580feb8565f7b6195b24535ca8201b4bcac58da063b32c47220d",
"org.stellaops.provenance.nonce": "a608acf859cd58a8389816b8d9eb2a07",
"org.stellaops.license.id": "lic-123",
"org.opencontainers.image.title": "sample.cdx.json",
"org.stellaops.repository": "git.stella-ops.org/stellaops"
}
},
"provenance": {
"status": "pending",
"expectedDsseSha256": "sha256:1b364a6b888d580feb8565f7b6195b24535ca8201b4bcac58da063b32c47220d",
"nonce": "a608acf859cd58a8389816b8d9eb2a07",
"attestorUri": "https://attestor.local/api/v1/provenance",
"predicateType": "https://slsa.dev/provenance/v1"
},
"metadata": {
"sbomDigest": "sha256:d07d06ae82e1789a5b505731f3ec3add106e23a55395213c9a881c7e816c695c",
"sbomPath": "sample.cdx.json",
"sbomMediaType": "application/vnd.cyclonedx\u002Bjson",
"subjectMediaType": "application/vnd.oci.image.manifest.v1\u002Bjson",
"repository": "git.stella-ops.org/stellaops",
"buildRef": "refs/heads/main",
"attestorUri": "https://attestor.local/api/v1/provenance"
}
{
"schema": "stellaops.buildx.descriptor.v1",
"generatedAt": "2025-10-18T12:00:00\u002B00:00",
"generator": {
"name": "StellaOps.Scanner.Sbomer.BuildXPlugin",
"version": "1.2.3"
},
"subject": {
"mediaType": "application/vnd.oci.image.manifest.v1\u002Bjson",
"digest": "sha256:0123456789abcdef"
},
"artifact": {
"mediaType": "application/vnd.cyclonedx\u002Bjson",
"digest": "sha256:d07d06ae82e1789a5b505731f3ec3add106e23a55395213c9a881c7e816c695c",
"size": 45,
"annotations": {
"org.opencontainers.artifact.type": "application/vnd.stellaops.sbom.layer\u002Bjson",
"org.stellaops.scanner.version": "1.2.3",
"org.stellaops.sbom.kind": "inventory",
"org.stellaops.sbom.format": "cyclonedx-json",
"org.stellaops.provenance.status": "pending",
"org.stellaops.provenance.dsse.sha256": "sha256:35ab4784f3bad40bb0063b522939ac729cf43d2012059947c0e56475d682c05e",
"org.stellaops.provenance.nonce": "5e13230e3dcbc8be996d8132d92e8826",
"org.stellaops.license.id": "lic-123",
"org.opencontainers.image.title": "sample.cdx.json",
"org.stellaops.repository": "git.stella-ops.org/stellaops"
}
},
"provenance": {
"status": "pending",
"expectedDsseSha256": "sha256:35ab4784f3bad40bb0063b522939ac729cf43d2012059947c0e56475d682c05e",
"nonce": "5e13230e3dcbc8be996d8132d92e8826",
"attestorUri": "https://attestor.local/api/v1/provenance",
"predicateType": "https://slsa.dev/provenance/v1"
},
"metadata": {
"sbomDigest": "sha256:d07d06ae82e1789a5b505731f3ec3add106e23a55395213c9a881c7e816c695c",
"sbomPath": "sample.cdx.json",
"sbomMediaType": "application/vnd.cyclonedx\u002Bjson",
"subjectMediaType": "application/vnd.oci.image.manifest.v1\u002Bjson",
"repository": "git.stella-ops.org/stellaops",
"buildRef": "refs/heads/main",
"attestorUri": "https://attestor.local/api/v1/provenance"
}
}

View File

@@ -0,0 +1,95 @@
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Scanner.Sbomer.BuildXPlugin.Surface;
using StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.TestUtilities;
using Xunit;
namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.Surface;
public sealed class SurfaceManifestWriterTests
{
[Fact]
public async Task WriteAsync_PersistsArtifactsAndManifest()
{
await using var temp = new TempDirectory();
var fragmentsPath = Path.Combine(temp.Path, "layer-fragments.json");
await File.WriteAllTextAsync(fragmentsPath, "[]");
var graphPath = Path.Combine(temp.Path, "entrytrace-graph.json");
await File.WriteAllTextAsync(graphPath, "{\"nodes\":[],\"edges\":[]}");
var ndjsonPath = Path.Combine(temp.Path, "entrytrace.ndjson");
await File.WriteAllTextAsync(ndjsonPath, "{}\n{}");
var manifestOutputPath = Path.Combine(temp.Path, "out", "surface-manifest.json");
var options = new SurfaceOptions(
CacheRoot: temp.Path,
CacheBucket: "scanner-artifacts",
RootPrefix: "scanner",
Tenant: "tenant-a",
Component: "scanner.buildx",
ComponentVersion: "1.2.3",
WorkerInstance: "builder-01",
Attempt: 2,
ImageDigest: "sha256:feedface",
ScanId: "scan-123",
LayerFragmentsPath: fragmentsPath,
EntryTraceGraphPath: graphPath,
EntryTraceNdjsonPath: ndjsonPath,
ManifestOutputPath: manifestOutputPath);
var writer = new SurfaceManifestWriter(TimeProvider.System);
var result = await writer.WriteAsync(options, CancellationToken.None);
Assert.NotNull(result);
Assert.NotNull(result!.Document.Source);
Assert.Equal("tenant-a", result.Document.Tenant);
Assert.Equal("scanner.buildx", result.Document.Source!.Component);
Assert.Equal("1.2.3", result.Document.Source.Version);
Assert.Equal(3, result.Document.Artifacts.Count);
var kinds = result.Document.Artifacts.Select(a => a.Kind).ToHashSet();
Assert.Contains("entrytrace.graph", kinds);
Assert.Contains("entrytrace.ndjson", kinds);
Assert.Contains("layer.fragments", kinds);
Assert.True(File.Exists(result.ManifestPath));
Assert.True(File.Exists(manifestOutputPath));
foreach (var artifact in result.Artifacts)
{
Assert.True(File.Exists(artifact.FilePath));
Assert.False(string.IsNullOrWhiteSpace(artifact.ManifestArtifact.Uri));
Assert.StartsWith("cas://scanner-artifacts/", artifact.ManifestArtifact.Uri, StringComparison.Ordinal);
}
}
[Fact]
public async Task WriteAsync_NoArtifacts_ReturnsNull()
{
await using var temp = new TempDirectory();
var options = new SurfaceOptions(
CacheRoot: temp.Path,
CacheBucket: "scanner-artifacts",
RootPrefix: "scanner",
Tenant: "tenant-a",
Component: "scanner.buildx",
ComponentVersion: "1.0",
WorkerInstance: "builder-01",
Attempt: 1,
ImageDigest: "sha256:deadbeef",
ScanId: "scan-1",
LayerFragmentsPath: null,
EntryTraceGraphPath: null,
EntryTraceNdjsonPath: null,
ManifestOutputPath: null);
var writer = new SurfaceManifestWriter(TimeProvider.System);
var result = await writer.WriteAsync(options, CancellationToken.None);
Assert.Null(result);
}
}

View File

@@ -0,0 +1,23 @@
using System;
using System.IO;
namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.TestUtilities;
internal static class TestPathHelper
{
public static string FindRepositoryRoot()
{
var current = AppContext.BaseDirectory;
for (var i = 0; i < 15 && !string.IsNullOrWhiteSpace(current); i++)
{
if (File.Exists(Path.Combine(current, "global.json")))
{
return current;
}
current = Directory.GetParent(current)?.FullName;
}
throw new InvalidOperationException("Unable to locate repository root (global.json not found).");
}
}