617 lines
24 KiB
C#
617 lines
24 KiB
C#
// -----------------------------------------------------------------------------
|
|
// LayerSbomEndpointsTests.cs
|
|
// Sprint: SPRINT_20260106_003_001_SCANNER_perlayer_sbom_api
|
|
// Task: T016 - Integration tests for layer SBOM API
|
|
// Description: Integration tests for per-layer SBOM and composition recipe endpoints.
|
|
// -----------------------------------------------------------------------------
|
|
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Immutable;
|
|
using System.Net;
|
|
using System.Net.Http.Json;
|
|
using System.Text;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
|
using StellaOps.Scanner.Emit.Composition;
|
|
using StellaOps.Scanner.WebService.Contracts;
|
|
using StellaOps.Scanner.WebService.Domain;
|
|
using StellaOps.Scanner.WebService.Services;
|
|
using StellaOps.TestKit;
|
|
|
|
namespace StellaOps.Scanner.WebService.Tests;
|
|
|
|
[Trait("Category", TestCategories.Integration)]
|
|
public sealed class LayerSbomEndpointsTests
|
|
{
|
|
private const string BasePath = "/api/v1/scans";
|
|
|
|
#region List Layers Tests
|
|
|
|
[Fact]
|
|
public async Task ListLayers_WhenScanExists_ReturnsLayers()
|
|
{
|
|
var scanId = "scan-" + Guid.NewGuid().ToString("N");
|
|
var mockService = new InMemoryLayerSbomService();
|
|
mockService.AddScan(scanId, "sha256:image123", CreateTestLayers(3));
|
|
var stubCoordinator = new StubScanCoordinator();
|
|
stubCoordinator.AddScan(scanId, "sha256:image123");
|
|
|
|
using var factory = new ScannerApplicationFactory()
|
|
.WithOverrides(configureServices: services =>
|
|
{
|
|
services.RemoveAll<ILayerSbomService>();
|
|
services.RemoveAll<IScanCoordinator>();
|
|
services.AddSingleton<ILayerSbomService>(mockService);
|
|
services.AddSingleton<IScanCoordinator>(stubCoordinator);
|
|
});
|
|
using var client = factory.CreateClient();
|
|
|
|
var response = await client.GetAsync($"{BasePath}/{scanId}/layers");
|
|
|
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
|
var result = await response.Content.ReadFromJsonAsync<LayerListResponseDto>();
|
|
Assert.NotNull(result);
|
|
Assert.Equal(scanId, result!.ScanId);
|
|
Assert.Equal("sha256:image123", result.ImageDigest);
|
|
Assert.Equal(3, result.Layers.Count);
|
|
Assert.All(result.Layers, l => Assert.True(l.HasSbom));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ListLayers_WhenScanNotFound_Returns404()
|
|
{
|
|
using var factory = new ScannerApplicationFactory()
|
|
.WithOverrides(configureServices: services =>
|
|
{
|
|
services.RemoveAll<ILayerSbomService>();
|
|
services.AddSingleton<ILayerSbomService, InMemoryLayerSbomService>();
|
|
});
|
|
using var client = factory.CreateClient();
|
|
|
|
var response = await client.GetAsync($"{BasePath}/scan-not-found/layers");
|
|
|
|
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ListLayers_LayersOrderedByOrder()
|
|
{
|
|
var scanId = "scan-" + Guid.NewGuid().ToString("N");
|
|
var mockService = new InMemoryLayerSbomService();
|
|
var layers = new[]
|
|
{
|
|
CreateLayerSummary("sha256:layer2", 2, 15),
|
|
CreateLayerSummary("sha256:layer0", 0, 42),
|
|
CreateLayerSummary("sha256:layer1", 1, 8),
|
|
};
|
|
mockService.AddScan(scanId, "sha256:image123", layers);
|
|
var stubCoordinator = new StubScanCoordinator();
|
|
stubCoordinator.AddScan(scanId, "sha256:image123");
|
|
|
|
using var factory = new ScannerApplicationFactory()
|
|
.WithOverrides(configureServices: services =>
|
|
{
|
|
services.RemoveAll<ILayerSbomService>();
|
|
services.RemoveAll<IScanCoordinator>();
|
|
services.AddSingleton<ILayerSbomService>(mockService);
|
|
services.AddSingleton<IScanCoordinator>(stubCoordinator);
|
|
});
|
|
using var client = factory.CreateClient();
|
|
|
|
var response = await client.GetAsync($"{BasePath}/{scanId}/layers");
|
|
|
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
|
var result = await response.Content.ReadFromJsonAsync<LayerListResponseDto>();
|
|
Assert.NotNull(result);
|
|
// Verify layer order is as stored (service already orders by Order)
|
|
Assert.Equal(0, result!.Layers[0].Order);
|
|
Assert.Equal(1, result.Layers[1].Order);
|
|
Assert.Equal(2, result.Layers[2].Order);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Get Layer SBOM Tests
|
|
|
|
[Fact]
|
|
public async Task GetLayerSbom_DefaultFormat_ReturnsCycloneDx()
|
|
{
|
|
var scanId = "scan-" + Guid.NewGuid().ToString("N");
|
|
var layerDigest = "sha256:layer123";
|
|
var mockService = new InMemoryLayerSbomService();
|
|
mockService.AddScan(scanId, "sha256:image123", CreateTestLayers(1, layerDigest));
|
|
mockService.AddLayerSbom(scanId, layerDigest, "cdx", CreateTestSbomBytes("cyclonedx"));
|
|
mockService.AddLayerSbom(scanId, layerDigest, "spdx", CreateTestSbomBytes("spdx"));
|
|
var stubCoordinator = new StubScanCoordinator();
|
|
stubCoordinator.AddScan(scanId, "sha256:image123");
|
|
|
|
using var factory = new ScannerApplicationFactory()
|
|
.WithOverrides(configureServices: services =>
|
|
{
|
|
services.RemoveAll<ILayerSbomService>();
|
|
services.RemoveAll<IScanCoordinator>();
|
|
services.AddSingleton<ILayerSbomService>(mockService);
|
|
services.AddSingleton<IScanCoordinator>(stubCoordinator);
|
|
});
|
|
using var client = factory.CreateClient();
|
|
|
|
var response = await client.GetAsync($"{BasePath}/{scanId}/layers/{layerDigest}/sbom");
|
|
|
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
|
Assert.Contains("cyclonedx", response.Content.Headers.ContentType?.ToString());
|
|
var content = await response.Content.ReadAsStringAsync();
|
|
Assert.Contains("cyclonedx", content);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetLayerSbom_SpdxFormat_ReturnsSpdx()
|
|
{
|
|
var scanId = "scan-" + Guid.NewGuid().ToString("N");
|
|
var layerDigest = "sha256:layer123";
|
|
var mockService = new InMemoryLayerSbomService();
|
|
mockService.AddScan(scanId, "sha256:image123", CreateTestLayers(1, layerDigest));
|
|
mockService.AddLayerSbom(scanId, layerDigest, "cdx", CreateTestSbomBytes("cyclonedx"));
|
|
mockService.AddLayerSbom(scanId, layerDigest, "spdx", CreateTestSbomBytes("spdx"));
|
|
var stubCoordinator = new StubScanCoordinator();
|
|
stubCoordinator.AddScan(scanId, "sha256:image123");
|
|
|
|
using var factory = new ScannerApplicationFactory()
|
|
.WithOverrides(configureServices: services =>
|
|
{
|
|
services.RemoveAll<ILayerSbomService>();
|
|
services.RemoveAll<IScanCoordinator>();
|
|
services.AddSingleton<ILayerSbomService>(mockService);
|
|
services.AddSingleton<IScanCoordinator>(stubCoordinator);
|
|
});
|
|
using var client = factory.CreateClient();
|
|
|
|
var response = await client.GetAsync($"{BasePath}/{scanId}/layers/{layerDigest}/sbom?format=spdx");
|
|
|
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
|
Assert.Contains("spdx", response.Content.Headers.ContentType?.ToString());
|
|
var content = await response.Content.ReadAsStringAsync();
|
|
Assert.Contains("spdx", content);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetLayerSbom_SetsImmutableCacheHeaders()
|
|
{
|
|
var scanId = "scan-" + Guid.NewGuid().ToString("N");
|
|
var layerDigest = "sha256:layer123";
|
|
var mockService = new InMemoryLayerSbomService();
|
|
mockService.AddScan(scanId, "sha256:image123", CreateTestLayers(1, layerDigest));
|
|
mockService.AddLayerSbom(scanId, layerDigest, "cdx", CreateTestSbomBytes("cyclonedx"));
|
|
var stubCoordinator = new StubScanCoordinator();
|
|
stubCoordinator.AddScan(scanId, "sha256:image123");
|
|
|
|
using var factory = new ScannerApplicationFactory()
|
|
.WithOverrides(configureServices: services =>
|
|
{
|
|
services.RemoveAll<ILayerSbomService>();
|
|
services.RemoveAll<IScanCoordinator>();
|
|
services.AddSingleton<ILayerSbomService>(mockService);
|
|
services.AddSingleton<IScanCoordinator>(stubCoordinator);
|
|
});
|
|
using var client = factory.CreateClient();
|
|
|
|
var response = await client.GetAsync($"{BasePath}/{scanId}/layers/{layerDigest}/sbom");
|
|
|
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
|
Assert.NotNull(response.Headers.ETag);
|
|
Assert.Contains("immutable", response.Headers.CacheControl?.ToString());
|
|
Assert.True(response.Headers.Contains("X-StellaOps-Layer-Digest"));
|
|
Assert.True(response.Headers.Contains("X-StellaOps-Format"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetLayerSbom_WhenScanNotFound_Returns404()
|
|
{
|
|
using var factory = new ScannerApplicationFactory()
|
|
.WithOverrides(configureServices: services =>
|
|
{
|
|
services.RemoveAll<ILayerSbomService>();
|
|
services.AddSingleton<ILayerSbomService, InMemoryLayerSbomService>();
|
|
});
|
|
using var client = factory.CreateClient();
|
|
|
|
var response = await client.GetAsync($"{BasePath}/scan-not-found/layers/sha256:layer123/sbom");
|
|
|
|
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetLayerSbom_WhenLayerNotFound_Returns404()
|
|
{
|
|
var scanId = "scan-" + Guid.NewGuid().ToString("N");
|
|
var mockService = new InMemoryLayerSbomService();
|
|
mockService.AddScan(scanId, "sha256:image123", CreateTestLayers(1));
|
|
|
|
using var factory = new ScannerApplicationFactory()
|
|
.WithOverrides(configureServices: services =>
|
|
{
|
|
services.RemoveAll<ILayerSbomService>();
|
|
services.AddSingleton<ILayerSbomService>(mockService);
|
|
});
|
|
using var client = factory.CreateClient();
|
|
|
|
var response = await client.GetAsync($"{BasePath}/{scanId}/layers/sha256:nonexistent/sbom");
|
|
|
|
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Composition Recipe Tests
|
|
|
|
[Fact]
|
|
public async Task GetCompositionRecipe_WhenExists_ReturnsRecipe()
|
|
{
|
|
var scanId = "scan-" + Guid.NewGuid().ToString("N");
|
|
var mockService = new InMemoryLayerSbomService();
|
|
mockService.AddScan(scanId, "sha256:image123", CreateTestLayers(2));
|
|
mockService.AddCompositionRecipe(scanId, CreateTestRecipe(scanId, "sha256:image123", 2));
|
|
|
|
using var factory = new ScannerApplicationFactory()
|
|
.WithOverrides(configureServices: services =>
|
|
{
|
|
services.RemoveAll<ILayerSbomService>();
|
|
services.AddSingleton<ILayerSbomService>(mockService);
|
|
});
|
|
using var client = factory.CreateClient();
|
|
|
|
var response = await client.GetAsync($"{BasePath}/{scanId}/composition-recipe");
|
|
|
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
|
var result = await response.Content.ReadFromJsonAsync<CompositionRecipeResponseDto>();
|
|
Assert.NotNull(result);
|
|
Assert.Equal(scanId, result!.ScanId);
|
|
Assert.Equal("sha256:image123", result.ImageDigest);
|
|
Assert.NotNull(result.Recipe);
|
|
Assert.Equal(2, result.Recipe.Layers.Count);
|
|
Assert.NotNull(result.Recipe.MerkleRoot);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetCompositionRecipe_WhenScanNotFound_Returns404()
|
|
{
|
|
using var factory = new ScannerApplicationFactory()
|
|
.WithOverrides(configureServices: services =>
|
|
{
|
|
services.RemoveAll<ILayerSbomService>();
|
|
services.AddSingleton<ILayerSbomService, InMemoryLayerSbomService>();
|
|
});
|
|
using var client = factory.CreateClient();
|
|
|
|
var response = await client.GetAsync($"{BasePath}/scan-not-found/composition-recipe");
|
|
|
|
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetCompositionRecipe_WhenRecipeNotAvailable_Returns404()
|
|
{
|
|
var scanId = "scan-" + Guid.NewGuid().ToString("N");
|
|
var mockService = new InMemoryLayerSbomService();
|
|
mockService.AddScan(scanId, "sha256:image123", CreateTestLayers(1));
|
|
// Note: not adding composition recipe
|
|
|
|
using var factory = new ScannerApplicationFactory()
|
|
.WithOverrides(configureServices: services =>
|
|
{
|
|
services.RemoveAll<ILayerSbomService>();
|
|
services.AddSingleton<ILayerSbomService>(mockService);
|
|
});
|
|
using var client = factory.CreateClient();
|
|
|
|
var response = await client.GetAsync($"{BasePath}/{scanId}/composition-recipe");
|
|
|
|
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Verify Composition Recipe Tests
|
|
|
|
[Fact]
|
|
public async Task VerifyCompositionRecipe_WhenValid_ReturnsSuccess()
|
|
{
|
|
var scanId = "scan-" + Guid.NewGuid().ToString("N");
|
|
var mockService = new InMemoryLayerSbomService();
|
|
mockService.AddScan(scanId, "sha256:image123", CreateTestLayers(2));
|
|
mockService.SetVerificationResult(scanId, new CompositionRecipeVerificationResult
|
|
{
|
|
Valid = true,
|
|
MerkleRootMatch = true,
|
|
LayerDigestsMatch = true,
|
|
Errors = ImmutableArray<string>.Empty,
|
|
});
|
|
|
|
using var factory = new ScannerApplicationFactory()
|
|
.WithOverrides(configureServices: services =>
|
|
{
|
|
services.RemoveAll<ILayerSbomService>();
|
|
services.AddSingleton<ILayerSbomService>(mockService);
|
|
});
|
|
using var client = factory.CreateClient();
|
|
|
|
var response = await client.PostAsync($"{BasePath}/{scanId}/composition-recipe/verify", null);
|
|
|
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
|
var result = await response.Content.ReadFromJsonAsync<CompositionRecipeVerificationResponseDto>();
|
|
Assert.NotNull(result);
|
|
Assert.True(result!.Valid);
|
|
Assert.True(result.MerkleRootMatch);
|
|
Assert.True(result.LayerDigestsMatch);
|
|
Assert.Null(result.Errors);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task VerifyCompositionRecipe_WhenInvalid_ReturnsErrors()
|
|
{
|
|
var scanId = "scan-" + Guid.NewGuid().ToString("N");
|
|
var mockService = new InMemoryLayerSbomService();
|
|
mockService.AddScan(scanId, "sha256:image123", CreateTestLayers(2));
|
|
mockService.SetVerificationResult(scanId, new CompositionRecipeVerificationResult
|
|
{
|
|
Valid = false,
|
|
MerkleRootMatch = false,
|
|
LayerDigestsMatch = true,
|
|
Errors = ImmutableArray.Create("Merkle root mismatch: expected sha256:abc, got sha256:def"),
|
|
});
|
|
|
|
using var factory = new ScannerApplicationFactory()
|
|
.WithOverrides(configureServices: services =>
|
|
{
|
|
services.RemoveAll<ILayerSbomService>();
|
|
services.AddSingleton<ILayerSbomService>(mockService);
|
|
});
|
|
using var client = factory.CreateClient();
|
|
|
|
var response = await client.PostAsync($"{BasePath}/{scanId}/composition-recipe/verify", null);
|
|
|
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
|
var result = await response.Content.ReadFromJsonAsync<CompositionRecipeVerificationResponseDto>();
|
|
Assert.NotNull(result);
|
|
Assert.False(result!.Valid);
|
|
Assert.False(result.MerkleRootMatch);
|
|
Assert.NotNull(result.Errors);
|
|
Assert.Single(result.Errors!);
|
|
Assert.Contains("Merkle root mismatch", result.Errors![0]);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task VerifyCompositionRecipe_WhenScanNotFound_Returns404()
|
|
{
|
|
using var factory = new ScannerApplicationFactory()
|
|
.WithOverrides(configureServices: services =>
|
|
{
|
|
services.RemoveAll<ILayerSbomService>();
|
|
services.AddSingleton<ILayerSbomService, InMemoryLayerSbomService>();
|
|
});
|
|
using var client = factory.CreateClient();
|
|
|
|
var response = await client.PostAsync($"{BasePath}/scan-not-found/composition-recipe/verify", null);
|
|
|
|
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Test Helpers
|
|
|
|
private static LayerSummary[] CreateTestLayers(int count, string? specificDigest = null)
|
|
{
|
|
var layers = new LayerSummary[count];
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
layers[i] = CreateLayerSummary(
|
|
i == 0 && specificDigest != null ? specificDigest : $"sha256:layer{i}",
|
|
i,
|
|
10 + i * 5);
|
|
}
|
|
return layers;
|
|
}
|
|
|
|
private static LayerSummary CreateLayerSummary(string digest, int order, int componentCount)
|
|
{
|
|
return new LayerSummary
|
|
{
|
|
LayerDigest = digest,
|
|
Order = order,
|
|
HasSbom = true,
|
|
ComponentCount = componentCount,
|
|
};
|
|
}
|
|
|
|
private static byte[] CreateTestSbomBytes(string format)
|
|
{
|
|
var content = format == "spdx"
|
|
? """{"spdxVersion":"SPDX-3.0.1","format":"spdx"}"""
|
|
: """{"bomFormat":"CycloneDX","specVersion":"1.7","format":"cyclonedx"}""";
|
|
return Encoding.UTF8.GetBytes(content);
|
|
}
|
|
|
|
private static CompositionRecipeResponse CreateTestRecipe(string scanId, string imageDigest, int layerCount)
|
|
{
|
|
var layers = new CompositionRecipeLayer[layerCount];
|
|
for (int i = 0; i < layerCount; i++)
|
|
{
|
|
layers[i] = new CompositionRecipeLayer
|
|
{
|
|
Digest = $"sha256:layer{i}",
|
|
Order = i,
|
|
FragmentDigest = $"sha256:frag{i}",
|
|
SbomDigests = new LayerSbomDigests
|
|
{
|
|
CycloneDx = $"sha256:cdx{i}",
|
|
Spdx = $"sha256:spdx{i}",
|
|
},
|
|
ComponentCount = 10 + i * 5,
|
|
};
|
|
}
|
|
|
|
return new CompositionRecipeResponse
|
|
{
|
|
ScanId = scanId,
|
|
ImageDigest = imageDigest,
|
|
CreatedAt = DateTimeOffset.UtcNow.ToString("O"),
|
|
Recipe = new CompositionRecipe
|
|
{
|
|
Version = "1.0.0",
|
|
GeneratorName = "StellaOps.Scanner",
|
|
GeneratorVersion = "2026.04",
|
|
Layers = layers.ToImmutableArray(),
|
|
MerkleRoot = "sha256:merkleroot123",
|
|
AggregatedSbomDigests = new AggregatedSbomDigests
|
|
{
|
|
CycloneDx = "sha256:finalcdx",
|
|
Spdx = "sha256:finalspdx",
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
|
|
/// <summary>
|
|
/// In-memory implementation of ILayerSbomService for testing.
|
|
/// </summary>
|
|
internal sealed class InMemoryLayerSbomService : ILayerSbomService
|
|
{
|
|
private readonly Dictionary<string, (string ImageDigest, LayerSummary[] Layers)> _scans = new(StringComparer.OrdinalIgnoreCase);
|
|
private readonly Dictionary<(string ScanId, string LayerDigest, string Format), byte[]> _layerSboms = new();
|
|
private readonly Dictionary<string, CompositionRecipeResponse> _recipes = new(StringComparer.OrdinalIgnoreCase);
|
|
private readonly Dictionary<string, CompositionRecipeVerificationResult> _verificationResults = new(StringComparer.OrdinalIgnoreCase);
|
|
|
|
public void AddScan(string scanId, string imageDigest, LayerSummary[] layers)
|
|
{
|
|
_scans[scanId] = (imageDigest, layers);
|
|
}
|
|
|
|
public bool HasScan(string scanId) => _scans.ContainsKey(scanId);
|
|
|
|
public (string ImageDigest, LayerSummary[] Layers)? GetScanData(string scanId)
|
|
{
|
|
if (_scans.TryGetValue(scanId, out var data))
|
|
return data;
|
|
return null;
|
|
}
|
|
|
|
public void AddLayerSbom(string scanId, string layerDigest, string format, byte[] sbomBytes)
|
|
{
|
|
_layerSboms[(scanId, layerDigest, format)] = sbomBytes;
|
|
}
|
|
|
|
public void AddCompositionRecipe(string scanId, CompositionRecipeResponse recipe)
|
|
{
|
|
_recipes[scanId] = recipe;
|
|
}
|
|
|
|
public void SetVerificationResult(string scanId, CompositionRecipeVerificationResult result)
|
|
{
|
|
_verificationResults[scanId] = result;
|
|
}
|
|
|
|
public Task<ImmutableArray<LayerSummary>> GetLayerSummariesAsync(
|
|
ScanId scanId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (!_scans.TryGetValue(scanId.Value, out var scanData))
|
|
{
|
|
return Task.FromResult(ImmutableArray<LayerSummary>.Empty);
|
|
}
|
|
|
|
return Task.FromResult(scanData.Layers.OrderBy(l => l.Order).ToImmutableArray());
|
|
}
|
|
|
|
public Task<byte[]?> GetLayerSbomAsync(
|
|
ScanId scanId,
|
|
string layerDigest,
|
|
string format,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (_layerSboms.TryGetValue((scanId.Value, layerDigest, format), out var sbomBytes))
|
|
{
|
|
return Task.FromResult<byte[]?>(sbomBytes);
|
|
}
|
|
|
|
return Task.FromResult<byte[]?>(null);
|
|
}
|
|
|
|
public Task<CompositionRecipeResponse?> GetCompositionRecipeAsync(
|
|
ScanId scanId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (_recipes.TryGetValue(scanId.Value, out var recipe))
|
|
{
|
|
return Task.FromResult<CompositionRecipeResponse?>(recipe);
|
|
}
|
|
|
|
return Task.FromResult<CompositionRecipeResponse?>(null);
|
|
}
|
|
|
|
public Task<CompositionRecipeVerificationResult?> VerifyCompositionRecipeAsync(
|
|
ScanId scanId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (_verificationResults.TryGetValue(scanId.Value, out var result))
|
|
{
|
|
return Task.FromResult<CompositionRecipeVerificationResult?>(result);
|
|
}
|
|
|
|
return Task.FromResult<CompositionRecipeVerificationResult?>(null);
|
|
}
|
|
|
|
public Task StoreLayerSbomsAsync(
|
|
ScanId scanId,
|
|
string imageDigest,
|
|
LayerSbomCompositionResult result,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
// Not implemented for tests
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stub IScanCoordinator that returns snapshots for registered scans.
|
|
/// </summary>
|
|
internal sealed class StubScanCoordinator : IScanCoordinator
|
|
{
|
|
private readonly ConcurrentDictionary<string, ScanSnapshot> _scans = new(StringComparer.OrdinalIgnoreCase);
|
|
|
|
public void AddScan(string scanId, string imageDigest)
|
|
{
|
|
var snapshot = new ScanSnapshot(
|
|
ScanId.Parse(scanId),
|
|
new ScanTarget("test-image", imageDigest, null),
|
|
ScanStatus.Completed,
|
|
DateTimeOffset.UtcNow.AddMinutes(-5),
|
|
DateTimeOffset.UtcNow,
|
|
null, null, null);
|
|
_scans[scanId] = snapshot;
|
|
}
|
|
|
|
public ValueTask<ScanSubmissionResult> SubmitAsync(ScanSubmission submission, CancellationToken cancellationToken)
|
|
=> throw new NotImplementedException();
|
|
|
|
public ValueTask<ScanSnapshot?> GetAsync(ScanId scanId, CancellationToken cancellationToken)
|
|
{
|
|
if (_scans.TryGetValue(scanId.Value, out var snapshot))
|
|
{
|
|
return ValueTask.FromResult<ScanSnapshot?>(snapshot);
|
|
}
|
|
return ValueTask.FromResult<ScanSnapshot?>(null);
|
|
}
|
|
|
|
public ValueTask<ScanSnapshot?> TryFindByTargetAsync(string? reference, string? digest, CancellationToken cancellationToken)
|
|
=> ValueTask.FromResult<ScanSnapshot?>(null);
|
|
|
|
public ValueTask<bool> AttachReplayAsync(ScanId scanId, ReplayArtifacts replay, CancellationToken cancellationToken)
|
|
=> ValueTask.FromResult(false);
|
|
|
|
public ValueTask<bool> AttachEntropyAsync(ScanId scanId, EntropySnapshot entropy, CancellationToken cancellationToken)
|
|
=> ValueTask.FromResult(false);
|
|
}
|