217 lines
8.2 KiB
C#
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);
|
|
}
|
|
}
|