chore(sprints): archive 20260226 advisories and expand deterministic tests

This commit is contained in:
master
2026-03-04 03:09:23 +02:00
parent 4fe8eb56ae
commit aaad8104cb
35 changed files with 4686 additions and 1 deletions

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}