chore(sprints): archive 20260226 advisories and expand deterministic tests
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Scanner.Storage.Oci;
|
||||
using StellaOps.Scanner.WebService.Contracts;
|
||||
using StellaOps.Scanner.WebService.Options;
|
||||
using StellaOps.Scanner.WebService.Services;
|
||||
using StellaOps.TestKit;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Tests;
|
||||
|
||||
public sealed class OciAttestationPublisherTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task PublishAsync_WhenDisabled_ReturnsSkippedDeterministically()
|
||||
{
|
||||
var handler = new OciRegistryHandler();
|
||||
using var httpClient = new HttpClient(handler);
|
||||
var pusher = new OciArtifactPusher(
|
||||
httpClient,
|
||||
CryptoHashFactory.CreateDefault(),
|
||||
new OciRegistryOptions
|
||||
{
|
||||
DefaultRegistry = "registry.example"
|
||||
},
|
||||
NullLogger<OciArtifactPusher>.Instance);
|
||||
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new ScannerWebServiceOptions
|
||||
{
|
||||
AttestationAttachment = new ScannerWebServiceOptions.AttestationAttachmentOptions
|
||||
{
|
||||
AutoAttach = false
|
||||
}
|
||||
});
|
||||
|
||||
var publisher = new OciAttestationPublisher(
|
||||
options,
|
||||
pusher,
|
||||
NullLogger<OciAttestationPublisher>.Instance);
|
||||
|
||||
var report = new ReportDocumentDto
|
||||
{
|
||||
ReportId = "report-disabled",
|
||||
ImageDigest = "registry.example/stellaops/demo@sha256:subjectdigest",
|
||||
GeneratedAt = DateTimeOffset.Parse("2026-02-26T00:00:00Z"),
|
||||
Verdict = "allow"
|
||||
};
|
||||
|
||||
var result = await publisher.PublishAsync(report, envelope: null, tenant: "tenant-a", CancellationToken.None);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal(0, result.AttachmentCount);
|
||||
Assert.Empty(result.Digests);
|
||||
Assert.Equal("Attestation attachment disabled", result.Error);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task PublishAsync_WhenImageDigestMissing_ReturnsFailedDeterministically()
|
||||
{
|
||||
var handler = new OciRegistryHandler();
|
||||
using var httpClient = new HttpClient(handler);
|
||||
var pusher = new OciArtifactPusher(
|
||||
httpClient,
|
||||
CryptoHashFactory.CreateDefault(),
|
||||
new OciRegistryOptions
|
||||
{
|
||||
DefaultRegistry = "registry.example"
|
||||
},
|
||||
NullLogger<OciArtifactPusher>.Instance);
|
||||
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new ScannerWebServiceOptions
|
||||
{
|
||||
AttestationAttachment = new ScannerWebServiceOptions.AttestationAttachmentOptions
|
||||
{
|
||||
AutoAttach = true
|
||||
}
|
||||
});
|
||||
|
||||
var publisher = new OciAttestationPublisher(
|
||||
options,
|
||||
pusher,
|
||||
NullLogger<OciAttestationPublisher>.Instance);
|
||||
|
||||
var report = new ReportDocumentDto
|
||||
{
|
||||
ReportId = "report-missing-image",
|
||||
ImageDigest = "",
|
||||
GeneratedAt = DateTimeOffset.Parse("2026-02-26T00:00:00Z"),
|
||||
Verdict = "blocked"
|
||||
};
|
||||
|
||||
var result = await publisher.PublishAsync(report, envelope: null, tenant: "tenant-a", CancellationToken.None);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.Equal(0, result.AttachmentCount);
|
||||
Assert.Empty(result.Digests);
|
||||
Assert.Equal("Missing image digest", result.Error);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task PublishAsync_WhenEnabled_AttachesAttestationAndReturnsRealDigest()
|
||||
{
|
||||
var handler = new OciRegistryHandler();
|
||||
using var httpClient = new HttpClient(handler);
|
||||
var pusher = new OciArtifactPusher(
|
||||
httpClient,
|
||||
CryptoHashFactory.CreateDefault(),
|
||||
new OciRegistryOptions
|
||||
{
|
||||
DefaultRegistry = "registry.example"
|
||||
},
|
||||
NullLogger<OciArtifactPusher>.Instance);
|
||||
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new ScannerWebServiceOptions
|
||||
{
|
||||
AttestationAttachment = new ScannerWebServiceOptions.AttestationAttachmentOptions
|
||||
{
|
||||
AutoAttach = true,
|
||||
ReplaceExisting = false,
|
||||
PredicateTypes = new List<string>
|
||||
{
|
||||
"stellaops.io/predicates/scan-result@v1"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var publisher = new OciAttestationPublisher(
|
||||
options,
|
||||
pusher,
|
||||
NullLogger<OciAttestationPublisher>.Instance);
|
||||
|
||||
var report = new ReportDocumentDto
|
||||
{
|
||||
ReportId = "report-1",
|
||||
ImageDigest = "registry.example/stellaops/demo@sha256:subjectdigest",
|
||||
GeneratedAt = DateTimeOffset.Parse("2026-02-26T00:00:00Z"),
|
||||
Verdict = "blocked",
|
||||
Policy = new ReportPolicyDto
|
||||
{
|
||||
Digest = "sha256:policy"
|
||||
},
|
||||
Summary = new ReportSummaryDto
|
||||
{
|
||||
Total = 1,
|
||||
Blocked = 1
|
||||
}
|
||||
};
|
||||
|
||||
var envelope = new DsseEnvelopeDto
|
||||
{
|
||||
PayloadType = "application/vnd.stellaops.report+json",
|
||||
Payload = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("{}")),
|
||||
Signatures =
|
||||
[
|
||||
new DsseSignatureDto
|
||||
{
|
||||
KeyId = "key-1",
|
||||
Sig = "signature-value"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var result = await publisher.PublishAsync(report, envelope, "tenant-a", CancellationToken.None);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal(1, result.AttachmentCount);
|
||||
Assert.Single(result.Digests);
|
||||
Assert.StartsWith("sha256:", result.Digests[0], StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private sealed class OciRegistryHandler : HttpMessageHandler
|
||||
{
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
var path = request.RequestUri?.AbsolutePath ?? string.Empty;
|
||||
|
||||
if (request.Method == HttpMethod.Head && path.Contains("/manifests/", StringComparison.Ordinal))
|
||||
{
|
||||
return new HttpResponseMessage(HttpStatusCode.NotFound);
|
||||
}
|
||||
|
||||
if (request.Method == HttpMethod.Head && path.Contains("/blobs/", StringComparison.Ordinal))
|
||||
{
|
||||
return new HttpResponseMessage(HttpStatusCode.NotFound);
|
||||
}
|
||||
|
||||
if (request.Method == HttpMethod.Post && path.EndsWith("/blobs/uploads/", StringComparison.Ordinal))
|
||||
{
|
||||
var response = new HttpResponseMessage(HttpStatusCode.Accepted);
|
||||
response.Headers.Location = new Uri("/v2/stellaops/demo/blobs/uploads/upload-id", UriKind.Relative);
|
||||
return response;
|
||||
}
|
||||
|
||||
if (request.Method == HttpMethod.Put && path.Contains("/blobs/uploads/", StringComparison.Ordinal))
|
||||
{
|
||||
return new HttpResponseMessage(HttpStatusCode.Created);
|
||||
}
|
||||
|
||||
if (request.Method == HttpMethod.Put && path.Contains("/manifests/", StringComparison.Ordinal))
|
||||
{
|
||||
var response = new HttpResponseMessage(HttpStatusCode.Created);
|
||||
var manifestBytes = request.Content is null
|
||||
? Array.Empty<byte>()
|
||||
: await request.Content.ReadAsByteArrayAsync(cancellationToken);
|
||||
var digest = $"sha256:{Convert.ToHexString(SHA256.HashData(manifestBytes)).ToLowerInvariant()}";
|
||||
response.Headers.TryAddWithoutValidation("Docker-Content-Digest", digest);
|
||||
return response;
|
||||
}
|
||||
|
||||
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using StellaOps.Scanner.Explainability.Assumptions;
|
||||
using StellaOps.Scanner.Reachability.Stack;
|
||||
using StellaOps.Scanner.WebService.Endpoints;
|
||||
using StellaOps.TestKit;
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Tests;
|
||||
|
||||
public sealed class ReachabilityStackEndpointsTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetStack_WhenRepositoryNotConfigured_ReturnsNotImplemented()
|
||||
{
|
||||
await using var factory = ScannerApplicationFactory.CreateLightweight();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/v1/reachability/finding-123/stack");
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotImplemented, response.StatusCode);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetStack_WhenRepositoryConfigured_ReturnsPersistedStack()
|
||||
{
|
||||
var repository = new InMemoryReachabilityStackRepository();
|
||||
await repository.StoreAsync(CreateSampleStack("finding-abc"), CancellationToken.None);
|
||||
|
||||
await using var factory = ScannerApplicationFactory.CreateLightweight().WithOverrides(
|
||||
configureServices: services =>
|
||||
{
|
||||
services.RemoveAll<IReachabilityStackRepository>();
|
||||
services.AddSingleton<IReachabilityStackRepository>(repository);
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/v1/reachability/finding-abc/stack");
|
||||
var payload = JsonDocument.Parse(await response.Content.ReadAsStringAsync()).RootElement;
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal("finding-abc", payload.GetProperty("findingId").GetString());
|
||||
Assert.Equal("Exploitable", payload.GetProperty("verdict").GetString());
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetStack_WhenRepositoryConfiguredButFindingMissing_ReturnsNotFound()
|
||||
{
|
||||
var repository = new InMemoryReachabilityStackRepository();
|
||||
|
||||
await using var factory = ScannerApplicationFactory.CreateLightweight().WithOverrides(
|
||||
configureServices: services =>
|
||||
{
|
||||
services.RemoveAll<IReachabilityStackRepository>();
|
||||
services.AddSingleton<IReachabilityStackRepository>(repository);
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/v1/reachability/finding-missing/stack");
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetLayer_WhenRepositoryConfigured_ReturnsLayerPayload()
|
||||
{
|
||||
var repository = new InMemoryReachabilityStackRepository();
|
||||
await repository.StoreAsync(CreateSampleStack("finding-layer"), CancellationToken.None);
|
||||
|
||||
await using var factory = ScannerApplicationFactory.CreateLightweight().WithOverrides(
|
||||
configureServices: services =>
|
||||
{
|
||||
services.RemoveAll<IReachabilityStackRepository>();
|
||||
services.AddSingleton<IReachabilityStackRepository>(repository);
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/v1/reachability/finding-layer/stack/layer/2");
|
||||
var payload = JsonDocument.Parse(await response.Content.ReadAsStringAsync()).RootElement;
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.True(payload.GetProperty("isResolved").GetBoolean());
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetLayer_WhenLayerNumberInvalid_ReturnsBadRequest()
|
||||
{
|
||||
var repository = new InMemoryReachabilityStackRepository();
|
||||
await repository.StoreAsync(CreateSampleStack("finding-invalid-layer"), CancellationToken.None);
|
||||
|
||||
await using var factory = ScannerApplicationFactory.CreateLightweight().WithOverrides(
|
||||
configureServices: services =>
|
||||
{
|
||||
services.RemoveAll<IReachabilityStackRepository>();
|
||||
services.AddSingleton<IReachabilityStackRepository>(repository);
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/v1/reachability/finding-invalid-layer/stack/layer/4");
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
private static ReachabilityStack CreateSampleStack(string findingId)
|
||||
{
|
||||
return new ReachabilityStack
|
||||
{
|
||||
Id = $"stack-{findingId}",
|
||||
FindingId = findingId,
|
||||
Symbol = new VulnerableSymbol(
|
||||
Name: "vulnerable_func",
|
||||
Library: "libdemo.so",
|
||||
Version: "1.0.0",
|
||||
VulnerabilityId: "CVE-2026-0001",
|
||||
Type: SymbolType.Function),
|
||||
StaticCallGraph = new ReachabilityLayer1
|
||||
{
|
||||
IsReachable = true,
|
||||
Confidence = ConfidenceLevel.High,
|
||||
AnalysisMethod = "unit-test"
|
||||
},
|
||||
BinaryResolution = new ReachabilityLayer2
|
||||
{
|
||||
IsResolved = true,
|
||||
Confidence = ConfidenceLevel.High,
|
||||
Reason = "resolved",
|
||||
Resolution = new SymbolResolution(
|
||||
SymbolName: "vulnerable_func",
|
||||
ResolvedLibrary: "libdemo.so",
|
||||
ResolvedVersion: "1.0.0",
|
||||
SymbolVersion: null,
|
||||
Method: ResolutionMethod.DirectLink)
|
||||
},
|
||||
RuntimeGating = new ReachabilityLayer3
|
||||
{
|
||||
IsGated = false,
|
||||
Outcome = GatingOutcome.NotGated,
|
||||
Confidence = ConfidenceLevel.High
|
||||
},
|
||||
Verdict = ReachabilityVerdict.Exploitable,
|
||||
AnalyzedAt = DateTimeOffset.Parse("2026-02-26T00:00:00Z"),
|
||||
Explanation = "deterministic-test"
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class InMemoryReachabilityStackRepository : IReachabilityStackRepository
|
||||
{
|
||||
private readonly Dictionary<string, ReachabilityStack> _items = new(StringComparer.Ordinal);
|
||||
|
||||
public Task<ReachabilityStack?> TryGetByFindingIdAsync(string findingId, CancellationToken ct)
|
||||
{
|
||||
_items.TryGetValue(findingId, out var stack);
|
||||
return Task.FromResult(stack);
|
||||
}
|
||||
|
||||
public Task StoreAsync(ReachabilityStack stack, CancellationToken ct)
|
||||
{
|
||||
_items[stack.FindingId] = stack;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user