release orchestration strengthening

This commit is contained in:
master
2026-01-17 21:32:03 +02:00
parent 195dff2457
commit da27b9faa9
256 changed files with 94634 additions and 2269 deletions

View File

@@ -24,7 +24,7 @@ public sealed class ApprovalEndpointsTests : IAsyncLifetime
private ScannerApplicationFactory _factory = null!;
private HttpClient _client = null!;
public async Task InitializeAsync()
public async ValueTask InitializeAsync()
{
_secrets = new TestSurfaceSecretsScope();
@@ -35,7 +35,7 @@ public sealed class ApprovalEndpointsTests : IAsyncLifetime
_client = _factory.CreateClient();
}
public async Task DisposeAsync()
public async ValueTask DisposeAsync()
{
_client.Dispose();
await _factory.DisposeAsync();

View File

@@ -9,7 +9,6 @@ using FluentAssertions;
using StellaOps.TestKit;
using StellaOps.TestKit.Fixtures;
using Xunit;
using Xunit.Abstractions;
namespace StellaOps.Scanner.WebService.Tests.Contract;
@@ -23,12 +22,10 @@ public sealed class ScannerOpenApiContractTests : IClassFixture<ScannerApplicati
{
private readonly ScannerApplicationFactory _factory;
private readonly string _snapshotPath;
private readonly ITestOutputHelper _output;
public ScannerOpenApiContractTests(ScannerApplicationFactory factory, ITestOutputHelper output)
public ScannerOpenApiContractTests(ScannerApplicationFactory factory)
{
_factory = factory;
_output = output;
_snapshotPath = Path.Combine(AppContext.BaseDirectory, "Contract", "Expected", "scanner-openapi.json");
}
@@ -79,15 +76,7 @@ public sealed class ScannerOpenApiContractTests : IClassFixture<ScannerApplicati
Assert.Fail(message);
}
// Log non-breaking changes for awareness
if (changes.NonBreakingChanges.Count > 0)
{
_output.WriteLine("Non-breaking API changes detected:");
foreach (var change in changes.NonBreakingChanges)
{
_output.WriteLine($" + {change}");
}
}
// Non-breaking changes are allowed in contract checks.
}
/// <summary>

View File

@@ -24,7 +24,7 @@ public sealed class EpssEndpointsTests : IAsyncLifetime
private ScannerApplicationFactory _factory = null!;
private HttpClient _client = null!;
public async Task InitializeAsync()
public async ValueTask InitializeAsync()
{
_secrets = new TestSurfaceSecretsScope();
_epssProvider = new InMemoryEpssProvider();
@@ -41,7 +41,7 @@ public sealed class EpssEndpointsTests : IAsyncLifetime
_client = _factory.CreateClient();
}
public async Task DisposeAsync()
public async ValueTask DisposeAsync()
{
_client.Dispose();
await _factory.DisposeAsync();

View File

@@ -84,8 +84,8 @@ public sealed class EvidenceBundleExporterBinaryDiffTests
using var reader = new StreamReader(deltaProofEntry.Open());
var content = await reader.ReadToEndAsync();
Assert.Contains("previousFingerprint", content);
Assert.Contains("currentFingerprint", content);
Assert.Contains("previousBinaryDigest", content);
Assert.Contains("currentBinaryDigest", content);
Assert.Contains("similarityScore", content);
}
@@ -181,54 +181,81 @@ public sealed class EvidenceBundleExporterBinaryDiffTests
CveId = "CVE-2026-1234",
ComponentPurl = "pkg:npm/lodash@4.17.21",
CacheKey = "cache-key-001",
Manifests = new ManifestsDto
Manifests = new ManifestHashesDto
{
ArtifactDigest = "sha256:abc123",
ManifestHash = "sha256:manifest",
FeedSnapshotHash = "sha256:feed",
PolicyHash = "sha256:policy"
}
},
Verification = new VerificationStatusDto
{
Status = "unknown",
HashesVerified = false,
AttestationsVerified = false,
EvidenceComplete = false
},
GeneratedAt = new DateTimeOffset(2026, 1, 15, 10, 30, 0, TimeSpan.Zero)
};
}
private static UnifiedEvidenceResponseDto CreateEvidenceWithBinaryDiff()
{
var evidence = CreateMinimalEvidence();
evidence.BinaryDiff = new BinaryDiffEvidenceDto
return CreateMinimalEvidence() with
{
Status = "available",
DiffType = "semantic",
PreviousBinaryDigest = "sha256:old123",
CurrentBinaryDigest = "sha256:new456",
SimilarityScore = 0.95,
FunctionChangeCount = 3,
SecurityChangeCount = 1
BinaryDiff = new BinaryDiffEvidenceDto
{
Status = "available",
DiffType = "semantic",
PreviousBinaryDigest = "sha256:old123",
CurrentBinaryDigest = "sha256:new456",
SimilarityScore = 0.95,
FunctionChangeCount = 3,
SecurityChangeCount = 1
}
};
return evidence;
}
private static UnifiedEvidenceResponseDto CreateEvidenceWithBinaryDiffAndAttestation()
{
var evidence = CreateEvidenceWithBinaryDiff();
evidence.BinaryDiff!.AttestationRef = new AttestationRefDto
return CreateMinimalEvidence() with
{
Id = "attest-12345",
RekorLogIndex = 123456789,
BundleDigest = "sha256:bundle123"
BinaryDiff = new BinaryDiffEvidenceDto
{
Status = "available",
DiffType = "semantic",
PreviousBinaryDigest = "sha256:old123",
CurrentBinaryDigest = "sha256:new456",
SimilarityScore = 0.95,
FunctionChangeCount = 3,
SecurityChangeCount = 1,
Attestation = new AttestationRefDto
{
Id = "attest-12345",
PredicateType = "https://stellaops.dev/attestation/binary-diff/v1",
RekorLogIndex = 123456789,
EnvelopeDigest = "sha256:bundle123"
}
}
};
return evidence;
}
private static UnifiedEvidenceResponseDto CreateEvidenceWithSemanticDiff()
{
var evidence = CreateEvidenceWithBinaryDiff();
evidence.BinaryDiff!.SemanticDiff = new BinarySemanticDiffDto
return CreateMinimalEvidence() with
{
PreviousFingerprint = "fp:abc123",
CurrentFingerprint = "fp:def456",
SimilarityScore = 0.92,
SemanticChanges = new List<string> { "control_flow_modified", "data_flow_changed" }
BinaryDiff = new BinaryDiffEvidenceDto
{
Status = "available",
DiffType = "semantic",
PreviousBinaryDigest = "sha256:old123",
CurrentBinaryDigest = "sha256:new456",
SimilarityScore = 0.95,
FunctionChangeCount = 3,
SecurityChangeCount = 1,
HasSemanticDiff = true,
SemanticSimilarity = 0.92
}
};
return evidence;
}
}

View File

@@ -328,7 +328,9 @@ public sealed class LayerSbomEndpointsTests
{
var scanId = "scan-" + Guid.NewGuid().ToString("N");
var mockService = new InMemoryLayerSbomService();
var coordinator = new StubScanCoordinator();
mockService.AddScan(scanId, "sha256:image123", CreateTestLayers(2));
coordinator.AddScan(scanId, "sha256:image123");
mockService.SetVerificationResult(scanId, new CompositionRecipeVerificationResult
{
Valid = true,
@@ -342,6 +344,8 @@ public sealed class LayerSbomEndpointsTests
{
services.RemoveAll<ILayerSbomService>();
services.AddSingleton<ILayerSbomService>(mockService);
services.RemoveAll<IScanCoordinator>();
services.AddSingleton<IScanCoordinator>(coordinator);
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
@@ -362,7 +366,9 @@ public sealed class LayerSbomEndpointsTests
{
var scanId = "scan-" + Guid.NewGuid().ToString("N");
var mockService = new InMemoryLayerSbomService();
var coordinator = new StubScanCoordinator();
mockService.AddScan(scanId, "sha256:image123", CreateTestLayers(2));
coordinator.AddScan(scanId, "sha256:image123");
mockService.SetVerificationResult(scanId, new CompositionRecipeVerificationResult
{
Valid = false,
@@ -376,6 +382,8 @@ public sealed class LayerSbomEndpointsTests
{
services.RemoveAll<ILayerSbomService>();
services.AddSingleton<ILayerSbomService>(mockService);
services.RemoveAll<IScanCoordinator>();
services.AddSingleton<IScanCoordinator>(coordinator);
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
@@ -400,6 +408,8 @@ public sealed class LayerSbomEndpointsTests
{
services.RemoveAll<ILayerSbomService>();
services.AddSingleton<ILayerSbomService, InMemoryLayerSbomService>();
services.RemoveAll<IScanCoordinator>();
services.AddSingleton<IScanCoordinator, StubScanCoordinator>();
});
await factory.InitializeAsync();
using var client = factory.CreateClient();

View File

@@ -6,6 +6,7 @@ using System.Text.Json;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using StellaOps.Attestation;
using StellaOps.Authority.Persistence.Postgres.Models;
using StellaOps.Authority.Persistence.Postgres.Repositories;
using Xunit;
@@ -61,7 +62,9 @@ public sealed class OfflineKitEndpointsTests
bundleContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
content.Add(bundleContent, "bundle", "bundle.tgz");
content.Add(new StringContent(dsseJson, Encoding.UTF8, "application/json"), "bundleSignature", "statement.dsse.json");
var bundleSignatureContent = new ByteArrayContent(Encoding.UTF8.GetBytes(dsseJson));
bundleSignatureContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
content.Add(bundleSignatureContent, "bundleSignature", "statement.dsse.json");
using var response = await client.PostAsync("/api/offline-kit/import", content);
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
@@ -127,7 +130,9 @@ public sealed class OfflineKitEndpointsTests
bundleContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
content.Add(bundleContent, "bundle", "bundle.tgz");
content.Add(new StringContent(invalidDsseJson, Encoding.UTF8, "application/json"), "bundleSignature", "statement.dsse.json");
var bundleSignatureContent = new ByteArrayContent(Encoding.UTF8.GetBytes(invalidDsseJson));
bundleSignatureContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
content.Add(bundleSignatureContent, "bundleSignature", "statement.dsse.json");
using var response = await client.PostAsync("/api/offline-kit/import", content);
Assert.Equal(HttpStatusCode.UnprocessableEntity, response.StatusCode);
@@ -178,7 +183,9 @@ public sealed class OfflineKitEndpointsTests
bundleContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
content.Add(bundleContent, "bundle", "bundle.tgz");
content.Add(new StringContent(invalidDsseJson, Encoding.UTF8, "application/json"), "bundleSignature", "statement.dsse.json");
var bundleSignatureContent = new ByteArrayContent(Encoding.UTF8.GetBytes(invalidDsseJson));
bundleSignatureContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
content.Add(bundleSignatureContent, "bundleSignature", "statement.dsse.json");
using var response = await client.PostAsync("/api/offline-kit/import", content);
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
@@ -609,7 +616,7 @@ public sealed class OfflineKitEndpointsTests
var payloadBase64 = Convert.ToBase64String(payloadBytes);
var payloadType = "application/vnd.in-toto+json";
var pae = BuildPae(payloadType, payloadBase64);
var pae = DsseHelper.PreAuthenticationEncoding(payloadType, payloadBytes);
var signature = rsa.SignData(pae, HashAlgorithmName.SHA256, RSASignaturePadding.Pss);
var signatureBase64 = Convert.ToBase64String(signature);
@@ -623,25 +630,6 @@ public sealed class OfflineKitEndpointsTests
return (fingerprint, pem.ToString(), dsseJson);
}
private static byte[] BuildPae(string payloadType, string payloadBase64)
{
var payloadText = Encoding.UTF8.GetString(Convert.FromBase64String(payloadBase64));
var parts = new[] { "DSSEv1", payloadType, payloadText };
var builder = new StringBuilder();
builder.Append("PAE:");
builder.Append(parts.Length);
foreach (var part in parts)
{
builder.Append(' ');
builder.Append(part.Length);
builder.Append(' ');
builder.Append(part);
}
return Encoding.UTF8.GetBytes(builder.ToString());
}
private sealed class TempDirectory : IDisposable
{
public TempDirectory()

View File

@@ -10,8 +10,8 @@ namespace StellaOps.Scanner.WebService.Tests;
public sealed class PlatformEventPublisherRegistrationTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void NullPublisherRegisteredWhenEventsDisabled()
[Fact]
public async Task NullPublisherRegisteredWhenEventsDisabled()
{
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
{
@@ -26,8 +26,8 @@ public sealed class PlatformEventPublisherRegistrationTests
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RedisPublisherRegisteredWhenEventsEnabled()
[Fact]
public async Task RedisPublisherRegisteredWhenEventsEnabled()
{
var originalEnabled = Environment.GetEnvironmentVariable("SCANNER__EVENTS__ENABLED");
var originalDriver = Environment.GetEnvironmentVariable("SCANNER__EVENTS__DRIVER");

View File

@@ -7,6 +7,7 @@
using Microsoft.Extensions.Time.Testing;
using StellaOps.Scanner.Reachability;
using StellaOps.Scanner.WebService.Services;
using StellaOps.Scanner.WebService.Domain;
namespace StellaOps.Scanner.WebService.Tests;
@@ -263,6 +264,24 @@ public sealed class PrAnnotationServiceTests
/// </summary>
private sealed class FakeReachabilityQueryService : IReachabilityQueryService
{
public Task<IReadOnlyList<ComponentReachability>> GetComponentsAsync(
ScanId scanId,
string? purlFilter,
string? statusFilter,
CancellationToken cancellationToken = default)
{
return Task.FromResult<IReadOnlyList<ComponentReachability>>(Array.Empty<ComponentReachability>());
}
public Task<IReadOnlyList<ReachabilityFinding>> GetFindingsAsync(
ScanId scanId,
string? cveFilter,
string? statusFilter,
CancellationToken cancellationToken = default)
{
return Task.FromResult<IReadOnlyList<ReachabilityFinding>>(Array.Empty<ReachabilityFinding>());
}
public Task<IReadOnlyDictionary<string, ReachabilityState>> GetReachabilityStatesAsync(
string graphId,
CancellationToken cancellationToken = default)

View File

@@ -104,10 +104,10 @@ public sealed class ScannerApplicationFactory : WebApplicationFactory<ServiceSta
return this;
}
public Task InitializeAsync()
public ValueTask InitializeAsync()
{
initializationTask ??= InitializeCoreAsync();
return initializationTask;
return new ValueTask(initializationTask);
}
private async Task InitializeCoreAsync()
@@ -135,9 +135,7 @@ public sealed class ScannerApplicationFactory : WebApplicationFactory<ServiceSta
initialized = true;
}
Task IAsyncLifetime.DisposeAsync() => DisposeAsync().AsTask();
public async ValueTask DisposeAsync()
public override async ValueTask DisposeAsync()
{
if (disposed)
{

View File

@@ -1,6 +1,5 @@
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Xunit;
namespace StellaOps.Scanner.WebService.Tests;
@@ -23,9 +22,9 @@ public sealed class ScannerApplicationFixture : IAsyncLifetime
return client;
}
public Task InitializeAsync() => Factory.InitializeAsync();
public ValueTask InitializeAsync() => Factory.InitializeAsync();
public async Task DisposeAsync()
public async ValueTask DisposeAsync()
{
_authenticatedFactory = null;
await Factory.DisposeAsync();

View File

@@ -25,7 +25,7 @@ public sealed class ScoreReplayEndpointsTests : IAsyncLifetime
private ScannerApplicationFactory _factory = null!;
private HttpClient _client = null!;
public async Task InitializeAsync()
public async ValueTask InitializeAsync()
{
_secrets = new TestSurfaceSecretsScope();
_factory = new ScannerApplicationFactory().WithOverrides(cfg =>
@@ -37,7 +37,7 @@ public sealed class ScoreReplayEndpointsTests : IAsyncLifetime
_client = _factory.CreateClient();
}
public async Task DisposeAsync()
public async ValueTask DisposeAsync()
{
_client.Dispose();
await _factory.DisposeAsync();

View File

@@ -558,7 +558,7 @@ public sealed class SignedSbomArchiveBuilderTests : IDisposable
return new SignedSbomArchiveRequest
{
ScanId = ScanId.CreateNew(),
ScanId = new ScanId("scan-test-001"),
SbomBytes = sbomBytes,
SbomFormat = "spdx-2.3",
DsseEnvelopeBytes = dsseBytes,

View File

@@ -4,9 +4,12 @@
using System.Net;
using System.Net.Http.Json;
using System.Net.Http.Headers;
using System.Text.Json;
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using StellaOps.Scanner.Emit.Composition;
using StellaOps.Scanner.Emit.Spdx;
using StellaOps.Scanner.WebService.Endpoints;
using StellaOps.Scanner.WebService.Services;
@@ -19,25 +22,43 @@ namespace StellaOps.Scanner.WebService.Tests;
/// Sprint: SPRINT_20260107_004_002 Task SG-015
/// </summary>
[Trait("Category", "Integration")]
public sealed class Spdx3ExportEndpointsTests : IClassFixture<ScannerApplicationFixture>
public sealed class Spdx3ExportEndpointsTests : IAsyncLifetime
{
private const string BasePath = "/api/scans";
private readonly ScannerApplicationFixture _fixture;
private const string BasePath = "/api/v1/scans";
private ScannerApplicationFactory _factory = null!;
private InMemoryLayerSbomService _layerSbomService = null!;
private HttpClient _client = null!;
public Spdx3ExportEndpointsTests(ScannerApplicationFixture fixture)
public async ValueTask InitializeAsync()
{
_fixture = fixture;
_layerSbomService = new InMemoryLayerSbomService();
_factory = new ScannerApplicationFactory().WithOverrides(
configureServices: services =>
{
services.RemoveAll<ILayerSbomService>();
services.AddSingleton<ILayerSbomService>(_layerSbomService);
},
useTestAuthentication: true);
await _factory.InitializeAsync();
_client = _factory.CreateClient();
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "test.valid.token");
}
public async ValueTask DisposeAsync()
{
_client.Dispose();
await _factory.DisposeAsync();
}
[Fact]
public async Task GetSbomExport_WithFormatSpdx3_ReturnsSpdx3Document()
{
// Arrange
var client = _fixture.CreateAuthenticatedClient();
var scanId = await CreateScanWithSbomAsync(client);
var scanId = await CreateScanWithSbomAsync();
// Act
var response = await client.GetAsync($"{BasePath}/{scanId}/exports/sbom?format=spdx3");
var response = await _client.GetAsync($"{BasePath}/{scanId}/exports/sbom?format=spdx3");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
@@ -59,11 +80,10 @@ public sealed class Spdx3ExportEndpointsTests : IClassFixture<ScannerApplication
public async Task GetSbomExport_WithProfileLite_ReturnsLiteProfile()
{
// Arrange
var client = _fixture.CreateAuthenticatedClient();
var scanId = await CreateScanWithSbomAsync(client);
var scanId = await CreateScanWithSbomAsync();
// Act
var response = await client.GetAsync($"{BasePath}/{scanId}/exports/sbom?format=spdx3&profile=lite");
var response = await _client.GetAsync($"{BasePath}/{scanId}/exports/sbom?format=spdx3&profile=lite");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
@@ -85,11 +105,10 @@ public sealed class Spdx3ExportEndpointsTests : IClassFixture<ScannerApplication
public async Task GetSbomExport_DefaultFormat_ReturnsSpdx2ForBackwardCompatibility()
{
// Arrange
var client = _fixture.CreateAuthenticatedClient();
var scanId = await CreateScanWithSbomAsync(client);
var scanId = await CreateScanWithSbomAsync();
// Act - no format specified
var response = await client.GetAsync($"{BasePath}/{scanId}/exports/sbom");
var response = await _client.GetAsync($"{BasePath}/{scanId}/exports/sbom");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
@@ -101,11 +120,10 @@ public sealed class Spdx3ExportEndpointsTests : IClassFixture<ScannerApplication
public async Task GetSbomExport_WithFormatCycloneDx_ReturnsCycloneDxDocument()
{
// Arrange
var client = _fixture.CreateAuthenticatedClient();
var scanId = await CreateScanWithSbomAsync(client);
var scanId = await CreateScanWithSbomAsync();
// Act
var response = await client.GetAsync($"{BasePath}/{scanId}/exports/sbom?format=cyclonedx");
var response = await _client.GetAsync($"{BasePath}/{scanId}/exports/sbom?format=cyclonedx");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
@@ -118,10 +136,8 @@ public sealed class Spdx3ExportEndpointsTests : IClassFixture<ScannerApplication
public async Task GetSbomExport_ScanNotFound_Returns404()
{
// Arrange
var client = _fixture.CreateAuthenticatedClient();
// Act
var response = await client.GetAsync($"{BasePath}/nonexistent-scan/exports/sbom?format=spdx3");
var response = await _client.GetAsync($"{BasePath}/nonexistent-scan/exports/sbom?format=spdx3");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
@@ -131,11 +147,10 @@ public sealed class Spdx3ExportEndpointsTests : IClassFixture<ScannerApplication
public async Task GetSbomExport_SoftwareProfile_IncludesLicenseInfo()
{
// Arrange
var client = _fixture.CreateAuthenticatedClient();
var scanId = await CreateScanWithSbomAsync(client);
var scanId = await CreateScanWithSbomAsync();
// Act
var response = await client.GetAsync($"{BasePath}/{scanId}/exports/sbom?format=spdx3&profile=software");
var response = await _client.GetAsync($"{BasePath}/{scanId}/exports/sbom?format=spdx3&profile=software");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
@@ -153,21 +168,57 @@ public sealed class Spdx3ExportEndpointsTests : IClassFixture<ScannerApplication
packages.Should().NotBeEmpty("Software profile should include package elements");
}
private async Task<string> CreateScanWithSbomAsync(HttpClient client)
private async Task<string> CreateScanWithSbomAsync()
{
// Create a scan via the API
var submitRequest = new
{
image = "registry.example.com/test:latest",
digest = "sha256:abc123def456abc123def456abc123def456abc123def456abc123def456abc1"
image = new
{
reference = "registry.example.com/test:latest",
digest = "sha256:abc123def456abc123def456abc123def456abc123def456abc123def456abc1"
}
};
var submitResponse = await client.PostAsJsonAsync($"{BasePath}/", submitRequest);
var submitResponse = await _client.PostAsJsonAsync($"{BasePath}/", submitRequest);
submitResponse.EnsureSuccessStatusCode();
var submitResult = await submitResponse.Content.ReadFromJsonAsync<JsonElement>();
var scanId = submitResult.GetProperty("scanId").GetString();
if (scanId is not null)
{
var layerDigest = $"sha256:{Guid.NewGuid():N}";
_layerSbomService.AddScan(scanId, submitRequest.image.digest, new[]
{
new LayerSummary
{
LayerDigest = layerDigest,
Order = 1,
HasSbom = true,
ComponentCount = 2
}
});
var spdx2Bytes = JsonSerializer.SerializeToUtf8Bytes(new
{
spdxVersion = "SPDX-2.3",
SPDXID = "SPDXRef-DOCUMENT",
name = "test",
packages = Array.Empty<object>()
});
_layerSbomService.AddLayerSbom(scanId, layerDigest, "spdx", spdx2Bytes);
var cdxBytes = JsonSerializer.SerializeToUtf8Bytes(new
{
bomFormat = "CycloneDX",
specVersion = "1.7",
version = 1,
components = Array.Empty<object>()
});
_layerSbomService.AddLayerSbom(scanId, layerDigest, "cdx", cdxBytes);
}
// Wait briefly for scan to initialize (in real tests, this would poll for completion)
await Task.Delay(100);