This commit is contained in:
StellaOps Bot
2025-12-18 20:37:27 +02:00
parent f85d53888c
commit 6410a6d082
17 changed files with 454 additions and 131 deletions

View File

@@ -1,5 +1,8 @@
using System.Collections.Generic;
using System.Formats.Cbor;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
@@ -10,6 +13,8 @@ namespace StellaOps.Scanner.WebService.Tests;
public sealed class ProofSpineEndpointsTests
{
private const string CborContentType = "application/cbor";
[Fact]
public async Task GetSpine_ReturnsSpine_WithVerification()
{
@@ -49,6 +54,42 @@ public sealed class ProofSpineEndpointsTests
Assert.True(body.TryGetProperty("verification", out _));
}
[Fact]
public async Task GetSpine_ReturnsCbor_WhenAcceptHeaderRequestsCbor()
{
await using var factory = new ScannerApplicationFactory();
using var scope = factory.Services.CreateScope();
var builder = scope.ServiceProvider.GetRequiredService<ProofSpineBuilder>();
var repository = scope.ServiceProvider.GetRequiredService<IProofSpineRepository>();
var spine = await builder
.ForArtifact("sha256:feedface")
.ForVulnerability("CVE-2025-1001")
.WithPolicyProfile("default")
.WithScanRun("scan-cbor-001")
.AddSbomSlice("sha256:sbom", new[] { "pkg:a", "pkg:b" }, toolId: "sbom", toolVersion: "1.0.0")
.BuildAsync();
await repository.SaveAsync(spine);
var client = factory.CreateClient();
using var request = new HttpRequestMessage(HttpMethod.Get, $"/api/v1/spines/{spine.SpineId}");
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(CborContentType));
using var response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(CborContentType, response.Content.Headers.ContentType?.MediaType);
var cbor = await response.Content.ReadAsByteArrayAsync();
var decoded = ReadRootMap(cbor);
Assert.Equal(spine.SpineId, decoded["spineId"]);
Assert.True(decoded.ContainsKey("verification"));
Assert.True(((List<object?>)decoded["segments"]!).Count > 0);
}
[Fact]
public async Task ListSpinesByScan_ReturnsSummaries_WithSegmentCount()
{
@@ -87,6 +128,44 @@ public sealed class ProofSpineEndpointsTests
Assert.True(items[0].GetProperty("segmentCount").GetInt32() > 0);
}
[Fact]
public async Task ListSpinesByScan_ReturnsCbor_WhenAcceptHeaderRequestsCbor()
{
await using var factory = new ScannerApplicationFactory();
using var scope = factory.Services.CreateScope();
var builder = scope.ServiceProvider.GetRequiredService<ProofSpineBuilder>();
var repository = scope.ServiceProvider.GetRequiredService<IProofSpineRepository>();
var spine = await builder
.ForArtifact("sha256:feedface")
.ForVulnerability("CVE-2025-1002")
.WithPolicyProfile("default")
.WithScanRun("scan-cbor-002")
.AddSbomSlice("sha256:sbom", new[] { "pkg:a" }, toolId: "sbom", toolVersion: "1.0.0")
.BuildAsync();
await repository.SaveAsync(spine);
var client = factory.CreateClient();
using var request = new HttpRequestMessage(HttpMethod.Get, "/api/v1/scans/scan-cbor-002/spines");
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(CborContentType));
using var response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(CborContentType, response.Content.Headers.ContentType?.MediaType);
var cbor = await response.Content.ReadAsByteArrayAsync();
var decoded = ReadRootMap(cbor);
var items = (List<object?>)decoded["items"]!;
Assert.Single(items);
var first = (Dictionary<string, object?>)items[0]!;
Assert.Equal(spine.SpineId, first["spineId"]);
Assert.True((int)first["segmentCount"]! > 0);
}
[Fact]
public async Task GetSpine_ReturnsInvalidStatus_WhenSegmentTampered()
{
@@ -125,4 +204,90 @@ public sealed class ProofSpineEndpointsTests
var segments = body.GetProperty("segments");
Assert.Equal("invalid", segments[0].GetProperty("status").GetString());
}
private static Dictionary<string, object?> ReadRootMap(byte[] cbor)
{
var reader = new CborReader(cbor, CborConformanceMode.Canonical);
return ReadMap(reader);
}
private static Dictionary<string, object?> ReadMap(CborReader reader)
{
var length = reader.ReadStartMap();
var result = new Dictionary<string, object?>(StringComparer.Ordinal);
if (length is not null)
{
for (var i = 0; i < length.Value; i++)
{
var key = reader.ReadTextString();
result[key] = ReadValue(reader);
}
reader.ReadEndMap();
return result;
}
while (reader.PeekState() != CborReaderState.EndMap)
{
var key = reader.ReadTextString();
result[key] = ReadValue(reader);
}
reader.ReadEndMap();
return result;
}
private static List<object?> ReadArray(CborReader reader)
{
var length = reader.ReadStartArray();
var result = new List<object?>();
if (length is not null)
{
for (var i = 0; i < length.Value; i++)
{
result.Add(ReadValue(reader));
}
reader.ReadEndArray();
return result;
}
while (reader.PeekState() != CborReaderState.EndArray)
{
result.Add(ReadValue(reader));
}
reader.ReadEndArray();
return result;
}
private static object? ReadValue(CborReader reader)
{
switch (reader.PeekState())
{
case CborReaderState.StartMap:
return ReadMap(reader);
case CborReaderState.StartArray:
return ReadArray(reader);
case CborReaderState.TextString:
return reader.ReadTextString();
case CborReaderState.UnsignedInteger:
return (int)reader.ReadUInt64();
case CborReaderState.NegativeInteger:
return (int)reader.ReadInt64();
case CborReaderState.DoublePrecisionFloat:
return reader.ReadDouble();
case CborReaderState.SinglePrecisionFloat:
return reader.ReadSingle();
case CborReaderState.HalfPrecisionFloat:
return reader.ReadHalf();
case CborReaderState.Boolean:
return reader.ReadBoolean();
case CborReaderState.Null:
reader.ReadNull();
return null;
default:
throw new InvalidOperationException($"Unexpected CBOR state {reader.PeekState()}");
}
}
}