save checkpoint
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using StellaOps.Scanner.Storage.ObjectStore;
|
||||
using StellaOps.Scanner.WebService.Contracts;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Tests;
|
||||
|
||||
public sealed class SbomHotLookupEndpointsTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task HotLookupEndpoints_ReturnLatestComponentAndPendingRows()
|
||||
{
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
await using var factory = await CreateFactoryAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var (scanId, payloadDigest) = await CreateScanAsync(client, "sha256:hotlookup0001");
|
||||
|
||||
var sbomJson = """
|
||||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.7",
|
||||
"version": 1,
|
||||
"components": [
|
||||
{
|
||||
"type": "library",
|
||||
"name": "openssl",
|
||||
"version": "3.0.12",
|
||||
"purl": "pkg:deb/debian/openssl@3.0.12"
|
||||
}
|
||||
],
|
||||
"vulnerabilities": [
|
||||
{
|
||||
"id": "CVE-2026-0001",
|
||||
"analysis": {
|
||||
"state": "triage_pending"
|
||||
},
|
||||
"affects": [
|
||||
{ "ref": "pkg:deb/debian/openssl@3.0.12" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
var submitResponse = await client.PostAsync(
|
||||
$"/api/v1/scans/{scanId}/sbom",
|
||||
new StringContent(sbomJson, Encoding.UTF8, "application/vnd.cyclonedx+json"));
|
||||
Assert.Equal(HttpStatusCode.Accepted, submitResponse.StatusCode);
|
||||
|
||||
var latest = await client.GetFromJsonAsync<SbomHotLookupLatestResponseDto>(
|
||||
$"/api/v1/sbom/hot-lookup/payload/{payloadDigest}/latest");
|
||||
Assert.NotNull(latest);
|
||||
Assert.Equal(payloadDigest, latest!.PayloadDigest);
|
||||
|
||||
var components = await client.GetFromJsonAsync<SbomHotLookupComponentSearchResponseDto>(
|
||||
"/api/v1/sbom/hot-lookup/components?purl=pkg:deb/debian/openssl@3.0.12&limit=20");
|
||||
Assert.NotNull(components);
|
||||
Assert.NotEmpty(components!.Items);
|
||||
|
||||
var pending = await client.GetFromJsonAsync<SbomHotLookupPendingSearchResponseDto>(
|
||||
"/api/v1/sbom/hot-lookup/pending-triage?limit=20");
|
||||
Assert.NotNull(pending);
|
||||
Assert.NotEmpty(pending!.Items);
|
||||
Assert.Equal(JsonValueKind.Array, pending.Items[0].Pending.ValueKind);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task HotLookupEndpoints_DuplicateCanonicalPayload_RemainsSingleProjectionRow()
|
||||
{
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
await using var factory = await CreateFactoryAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var (scanId, payloadDigest) = await CreateScanAsync(client, "sha256:hotlookup0002");
|
||||
const string queryPurl = "pkg:deb/debian/openssl@3.0.12";
|
||||
|
||||
var first = """
|
||||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.7",
|
||||
"version": 1,
|
||||
"components": [
|
||||
{ "name": "openssl", "version": "3.0.12", "purl": "pkg:deb/debian/openssl@3.0.12" }
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
var second = """
|
||||
{
|
||||
"specVersion": "1.7",
|
||||
"components": [
|
||||
{ "version": "3.0.12", "purl": "pkg:deb/debian/openssl@3.0.12", "name": "openssl" }
|
||||
],
|
||||
"version": 1,
|
||||
"bomFormat": "CycloneDX"
|
||||
}
|
||||
""";
|
||||
|
||||
var firstSubmit = await client.PostAsync(
|
||||
$"/api/v1/scans/{scanId}/sbom",
|
||||
new StringContent(first, Encoding.UTF8, "application/vnd.cyclonedx+json"));
|
||||
Assert.Equal(HttpStatusCode.Accepted, firstSubmit.StatusCode);
|
||||
|
||||
var firstLatest = await client.GetFromJsonAsync<SbomHotLookupLatestResponseDto>(
|
||||
$"/api/v1/sbom/hot-lookup/payload/{payloadDigest}/latest");
|
||||
Assert.NotNull(firstLatest);
|
||||
|
||||
var secondSubmit = await client.PostAsync(
|
||||
$"/api/v1/scans/{scanId}/sbom",
|
||||
new StringContent(second, Encoding.UTF8, "application/vnd.cyclonedx+json"));
|
||||
Assert.Equal(HttpStatusCode.Accepted, secondSubmit.StatusCode);
|
||||
|
||||
var secondLatest = await client.GetFromJsonAsync<SbomHotLookupLatestResponseDto>(
|
||||
$"/api/v1/sbom/hot-lookup/payload/{payloadDigest}/latest");
|
||||
Assert.NotNull(secondLatest);
|
||||
Assert.Equal(firstLatest!.CanonicalBomSha256, secondLatest!.CanonicalBomSha256);
|
||||
|
||||
var componentHits = await client.GetFromJsonAsync<SbomHotLookupComponentSearchResponseDto>(
|
||||
$"/api/v1/sbom/hot-lookup/components?purl={queryPurl}");
|
||||
Assert.NotNull(componentHits);
|
||||
Assert.Single(componentHits!.Items);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task HotLookupEndpoints_InvalidComponentQuery_ReturnsBadRequest()
|
||||
{
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
await using var factory = await CreateFactoryAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/v1/sbom/hot-lookup/components?purl=a&name=b");
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
private static async Task<ScannerApplicationFactory> CreateFactoryAsync()
|
||||
{
|
||||
var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
{
|
||||
configuration["scanner:authority:enabled"] = "false";
|
||||
}, configureServices: services =>
|
||||
{
|
||||
services.RemoveAll<IArtifactObjectStore>();
|
||||
services.AddSingleton<IArtifactObjectStore>(new InMemoryArtifactObjectStore());
|
||||
});
|
||||
|
||||
await factory.InitializeAsync();
|
||||
return factory;
|
||||
}
|
||||
|
||||
private static async Task<(string ScanId, string PayloadDigest)> CreateScanAsync(HttpClient client, string payloadDigest)
|
||||
{
|
||||
var response = await client.PostAsJsonAsync("/api/v1/scans", new ScanSubmitRequest
|
||||
{
|
||||
Image = new ScanImageDescriptor
|
||||
{
|
||||
Reference = "example.com/hotlookup:1.0",
|
||||
Digest = payloadDigest
|
||||
}
|
||||
});
|
||||
|
||||
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
|
||||
var payload = await response.Content.ReadFromJsonAsync<ScanSubmitResponse>();
|
||||
Assert.NotNull(payload);
|
||||
Assert.False(string.IsNullOrWhiteSpace(payload!.ScanId));
|
||||
|
||||
return (payload.ScanId, payloadDigest);
|
||||
}
|
||||
|
||||
private sealed class InMemoryArtifactObjectStore : IArtifactObjectStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, byte[]> _objects = new(StringComparer.Ordinal);
|
||||
|
||||
public async Task PutAsync(ArtifactObjectDescriptor descriptor, Stream content, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(descriptor);
|
||||
ArgumentNullException.ThrowIfNull(content);
|
||||
|
||||
using var buffer = new MemoryStream();
|
||||
await content.CopyToAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||||
_objects[$"{descriptor.Bucket}:{descriptor.Key}"] = buffer.ToArray();
|
||||
}
|
||||
|
||||
public Task<Stream?> GetAsync(ArtifactObjectDescriptor descriptor, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(descriptor);
|
||||
|
||||
if (!_objects.TryGetValue($"{descriptor.Bucket}:{descriptor.Key}", out var bytes))
|
||||
{
|
||||
return Task.FromResult<Stream?>(null);
|
||||
}
|
||||
|
||||
return Task.FromResult<Stream?>(new MemoryStream(bytes, writable: false));
|
||||
}
|
||||
|
||||
public Task DeleteAsync(ArtifactObjectDescriptor descriptor, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(descriptor);
|
||||
_objects.TryRemove($"{descriptor.Bucket}:{descriptor.Key}", out _);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user