Files
git.stella-ops.org/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/SliceQueryServiceRetrievalTests.cs

217 lines
8.2 KiB
C#

using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Cryptography;
using StellaOps.Replay.Core;
using StellaOps.Scanner.Cache;
using StellaOps.Scanner.Cache.Abstractions;
using StellaOps.Scanner.Cache.FileCas;
using StellaOps.Scanner.Core;
using StellaOps.Scanner.ProofSpine;
using StellaOps.Scanner.Reachability.Slices;
using StellaOps.Scanner.WebService.Services;
using StellaOps.TestKit;
using System.Collections.Immutable;
using System.Text;
using System.Text.Json;
using Xunit;
namespace StellaOps.Scanner.WebService.Tests;
public sealed class SliceQueryServiceRetrievalTests : IDisposable
{
private readonly string _tempRoot;
private readonly IFileContentAddressableStore _cas;
private readonly SliceQueryService _service;
public SliceQueryServiceRetrievalTests()
{
_tempRoot = Path.Combine(Path.GetTempPath(), $"stella-slice-query-{Guid.NewGuid():N}");
var scannerCacheOptions = Microsoft.Extensions.Options.Options.Create(new ScannerCacheOptions
{
RootPath = _tempRoot,
FileTtl = TimeSpan.FromDays(1),
MaxBytes = 1024 * 1024 * 10
});
_cas = new FileContentAddressableStore(
scannerCacheOptions,
NullLogger<FileContentAddressableStore>.Instance,
TimeProvider.System);
var cryptoHash = CryptoHashFactory.CreateDefault();
var sliceHasher = new SliceHasher(cryptoHash);
var sliceSigner = new SliceDsseSigner(
new TestDsseSigningService(),
new TestCryptoProfile(),
sliceHasher,
TimeProvider.System);
var casStorage = new SliceCasStorage(sliceHasher, sliceSigner, cryptoHash);
_service = new SliceQueryService(
cache: new SliceCache(Microsoft.Extensions.Options.Options.Create(new SliceCacheOptions())),
extractor: new SliceExtractor(new VerdictComputer()),
casStorage: casStorage,
diffComputer: new StellaOps.Scanner.Reachability.Slices.Replay.SliceDiffComputer(),
hasher: sliceHasher,
cas: _cas,
scannerCacheOptions: scannerCacheOptions,
scanRepo: new NullScanMetadataRepository(),
timeProvider: TimeProvider.System,
options: Microsoft.Extensions.Options.Options.Create(new SliceQueryServiceOptions()),
logger: NullLogger<SliceQueryService>.Instance);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task GetSliceAsync_WhenSliceExistsInCas_ReturnsSlice()
{
const string digestHex = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
var bytes = JsonSerializer.SerializeToUtf8Bytes(CreateSlice("scan-a"));
await _cas.PutAsync(new FileCasPutRequest(digestHex, new MemoryStream(bytes)));
var result = await _service.GetSliceAsync($"sha256:{digestHex}");
Assert.NotNull(result);
Assert.Equal("scan-a", result!.Manifest.ScanId);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task GetSliceAsync_WhenSliceMissingInCas_ReturnsNull()
{
var result = await _service.GetSliceAsync("sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
Assert.Null(result);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task GetSliceAsync_WhenSlicePayloadCorrupt_ThrowsDeterministicError()
{
const string digestHex = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc";
await _cas.PutAsync(new FileCasPutRequest(digestHex, new MemoryStream(Encoding.UTF8.GetBytes("not-json"))));
var action = async () => await _service.GetSliceAsync($"sha256:{digestHex}");
var ex = await Assert.ThrowsAsync<InvalidOperationException>(action);
Assert.Contains("corrupt", ex.Message, StringComparison.OrdinalIgnoreCase);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task GetSliceDsseAsync_WhenEnvelopeExists_ReturnsEnvelope()
{
const string digestHex = "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd";
var envelope = new DsseEnvelope(
PayloadType: "application/vnd.stellaops.slice+json",
Payload: Convert.ToBase64String(Encoding.UTF8.GetBytes("{}")),
Signatures: [new DsseSignature("k1", "s1")]);
var envelopeBytes = JsonSerializer.SerializeToUtf8Bytes(envelope);
await _cas.PutAsync(new FileCasPutRequest($"{digestHex}.dsse", new MemoryStream(envelopeBytes)));
var result = await _service.GetSliceDsseAsync($"sha256:{digestHex}");
Assert.NotNull(result);
var typed = Assert.IsType<DsseEnvelope>(result);
Assert.Equal("application/vnd.stellaops.slice+json", typed.PayloadType);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task GetSliceDsseAsync_WhenEnvelopeCorrupt_ThrowsDeterministicError()
{
const string digestHex = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
await _cas.PutAsync(new FileCasPutRequest($"{digestHex}.dsse", new MemoryStream(Encoding.UTF8.GetBytes("{broken-json"))));
var action = async () => await _service.GetSliceDsseAsync($"sha256:{digestHex}");
var ex = await Assert.ThrowsAsync<InvalidOperationException>(action);
Assert.Contains("corrupt", ex.Message, StringComparison.OrdinalIgnoreCase);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task GetSliceDsseAsync_WhenEnvelopeMissing_ReturnsNull()
{
var result = await _service.GetSliceDsseAsync("sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
Assert.Null(result);
}
public void Dispose()
{
try
{
Directory.Delete(_tempRoot, recursive: true);
}
catch (IOException)
{
}
catch (UnauthorizedAccessException)
{
}
}
private static ReachabilitySlice CreateSlice(string scanId)
{
return new ReachabilitySlice
{
Inputs = new SliceInputs
{
GraphDigest = "sha256:graph"
},
Query = new SliceQuery
{
CveId = "CVE-2026-0001",
TargetSymbols = ImmutableArray.Create("target")
},
Subgraph = new SliceSubgraph
{
Nodes = ImmutableArray<SliceNode>.Empty,
Edges = ImmutableArray<SliceEdge>.Empty
},
Verdict = new SliceVerdict
{
Status = SliceVerdictStatus.Unknown,
Confidence = 0.4
},
Manifest = ScanManifest.CreateBuilder(scanId, "sha256:artifact")
.WithScannerVersion("1.0.0")
.WithWorkerVersion("1.0.0")
.WithConcelierSnapshot("sha256:concelier")
.WithExcititorSnapshot("sha256:excititor")
.WithLatticePolicyHash("sha256:policy")
.Build()
};
}
private sealed class TestCryptoProfile : ICryptoProfile
{
public string KeyId => "test-key";
public string Algorithm => "hs256";
}
private sealed class TestDsseSigningService : IDsseSigningService
{
public Task<DsseEnvelope> SignAsync(object payload, string payloadType, ICryptoProfile cryptoProfile, CancellationToken cancellationToken = default)
{
var payloadBytes = CanonicalJson.SerializeToUtf8Bytes(payload);
var envelope = new DsseEnvelope(
PayloadType: payloadType,
Payload: Convert.ToBase64String(payloadBytes),
Signatures: [new DsseSignature(cryptoProfile.KeyId, "sig")]);
return Task.FromResult(envelope);
}
public Task<DsseVerificationOutcome> VerifyAsync(DsseEnvelope envelope, CancellationToken cancellationToken = default)
=> Task.FromResult(new DsseVerificationOutcome(true, true, null));
}
private sealed class NullScanMetadataRepository : IScanMetadataRepository
{
public Task<ScanMetadata?> GetScanMetadataAsync(string scanId, CancellationToken cancellationToken = default)
=> Task.FromResult<ScanMetadata?>(null);
}
}