save checkpoint: save features
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using StellaOps.Scanner.SmartDiff.Detection;
|
||||
using StellaOps.Scanner.WebService.Services;
|
||||
using StellaOps.TestKit;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Tests;
|
||||
|
||||
[Trait("Category", TestCategories.Integration)]
|
||||
public sealed class SmartDiffEndpointsTests
|
||||
{
|
||||
private const string BasePath = "/api/v1/smart-diff";
|
||||
private static readonly DateTimeOffset FixedNow = new(2026, 2, 12, 8, 0, 0, TimeSpan.Zero);
|
||||
|
||||
[Fact]
|
||||
public async Task GetScanScopedVexCandidates_ReturnsCandidatesForResolvedTargetDigest()
|
||||
{
|
||||
var imageDigest = "sha256:target-123";
|
||||
var candidate = CreateCandidate("candidate-1", imageDigest, requiresReview: true);
|
||||
var candidateStore = new InMemoryVexCandidateStore();
|
||||
await candidateStore.StoreCandidatesAsync([candidate], TestContext.Current.CancellationToken);
|
||||
|
||||
var metadataRepository = new StubScanMetadataRepository();
|
||||
metadataRepository.Set("scan-1", new ScanMetadata(null, imageDigest, FixedNow));
|
||||
|
||||
await using var factory = CreateFactory(candidateStore, metadataRepository);
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync($"{BasePath}/scan-1/vex-candidates", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
using var payload = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync(TestContext.Current.CancellationToken), cancellationToken: TestContext.Current.CancellationToken);
|
||||
Assert.Equal(imageDigest, payload.RootElement.GetProperty("imageDigest").GetString());
|
||||
Assert.Equal(1, payload.RootElement.GetProperty("totalCandidates").GetInt32());
|
||||
|
||||
var first = payload.RootElement.GetProperty("candidates")[0];
|
||||
Assert.Equal("candidate-1", first.GetProperty("candidateId").GetString());
|
||||
Assert.Equal("CVE-2026-0001", first.GetProperty("vulnId").GetString());
|
||||
Assert.Equal("vulnerable_code_not_in_execute_path", first.GetProperty("justification").GetString());
|
||||
Assert.True(first.GetProperty("requiresReview").GetBoolean());
|
||||
Assert.Equal("delta", first.GetProperty("evidenceLinks")[0].GetProperty("type").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReviewScanScopedCandidate_UpdatesCandidateReviewState()
|
||||
{
|
||||
var imageDigest = "sha256:target-456";
|
||||
var candidateStore = new InMemoryVexCandidateStore();
|
||||
await candidateStore.StoreCandidatesAsync(
|
||||
[CreateCandidate("candidate-2", imageDigest, requiresReview: true)],
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
var metadataRepository = new StubScanMetadataRepository();
|
||||
metadataRepository.Set("scan-2", new ScanMetadata(null, imageDigest, FixedNow));
|
||||
|
||||
await using var factory = CreateFactory(candidateStore, metadataRepository);
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var reviewResponse = await client.PostAsJsonAsync(
|
||||
$"{BasePath}/scan-2/vex-candidates/review",
|
||||
new ScanReviewRequestDto("candidate-2", "accept", "verified"),
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, reviewResponse.StatusCode);
|
||||
|
||||
var candidateResponse = await client.GetAsync($"{BasePath}/candidates/candidate-2", TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.OK, candidateResponse.StatusCode);
|
||||
|
||||
using var payload = await JsonDocument.ParseAsync(await candidateResponse.Content.ReadAsStreamAsync(TestContext.Current.CancellationToken), cancellationToken: TestContext.Current.CancellationToken);
|
||||
var candidate = payload.RootElement.GetProperty("candidate");
|
||||
Assert.False(candidate.GetProperty("requiresReview").GetBoolean());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetScanSarif_EmbedsVexCandidateResults()
|
||||
{
|
||||
var imageDigest = "sha256:target-789";
|
||||
var candidateStore = new InMemoryVexCandidateStore();
|
||||
await candidateStore.StoreCandidatesAsync(
|
||||
[CreateCandidate("candidate-3", imageDigest, requiresReview: false)],
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
var metadataRepository = new StubScanMetadataRepository();
|
||||
metadataRepository.Set("scan-3", new ScanMetadata("sha256:base-111", imageDigest, FixedNow));
|
||||
|
||||
await using var factory = CreateFactory(candidateStore, metadataRepository);
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync($"{BasePath}/scans/scan-3/sarif", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Contains("\"ruleId\":\"SDIFF003\"", body, StringComparison.Ordinal);
|
||||
Assert.Contains("CVE-2026-0001", body, StringComparison.Ordinal);
|
||||
Assert.Contains("pkg:npm/test-component@1.0.0", body, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static ScannerApplicationFactory CreateFactory(
|
||||
IVexCandidateStore candidateStore,
|
||||
IScanMetadataRepository metadataRepository)
|
||||
{
|
||||
return ScannerApplicationFactory.CreateLightweight()
|
||||
.WithOverrides(configureServices: services =>
|
||||
{
|
||||
services.RemoveAll<IVexCandidateStore>();
|
||||
services.RemoveAll<IMaterialRiskChangeRepository>();
|
||||
services.RemoveAll<IScanMetadataRepository>();
|
||||
|
||||
services.AddSingleton(candidateStore);
|
||||
services.AddSingleton<IMaterialRiskChangeRepository, StubMaterialRiskChangeRepository>();
|
||||
services.AddSingleton(metadataRepository);
|
||||
});
|
||||
}
|
||||
|
||||
private static VexCandidate CreateCandidate(string candidateId, string imageDigest, bool requiresReview)
|
||||
{
|
||||
return new VexCandidate(
|
||||
CandidateId: candidateId,
|
||||
FindingKey: new FindingKey("CVE-2026-0001", "pkg:npm/test-component@1.0.0"),
|
||||
SuggestedStatus: VexStatusType.NotAffected,
|
||||
Justification: VexJustification.VulnerableCodeNotInExecutePath,
|
||||
Rationale: "Reachability evidence shows vulnerable APIs are not reachable.",
|
||||
EvidenceLinks: ImmutableArray.Create(new EvidenceLink("delta", "oci://scanner/delta/scan-1", "sha256:evidence")),
|
||||
Confidence: 0.97,
|
||||
ImageDigest: imageDigest,
|
||||
GeneratedAt: FixedNow,
|
||||
ExpiresAt: FixedNow.AddDays(30),
|
||||
RequiresReview: requiresReview);
|
||||
}
|
||||
|
||||
private sealed record ScanReviewRequestDto(string CandidateId, string Action, string? Comment);
|
||||
|
||||
private sealed class StubScanMetadataRepository : IScanMetadataRepository
|
||||
{
|
||||
private readonly Dictionary<string, ScanMetadata> _metadataByScanId = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public void Set(string scanId, ScanMetadata metadata)
|
||||
{
|
||||
_metadataByScanId[scanId] = metadata;
|
||||
}
|
||||
|
||||
public Task<ScanMetadata?> GetScanMetadataAsync(string scanId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return Task.FromResult(_metadataByScanId.TryGetValue(scanId, out var metadata) ? metadata : null);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class StubMaterialRiskChangeRepository : IMaterialRiskChangeRepository
|
||||
{
|
||||
public Task StoreChangeAsync(MaterialRiskChangeResult change, string scanId, CancellationToken ct = default) => Task.CompletedTask;
|
||||
|
||||
public Task StoreChangesAsync(IReadOnlyList<MaterialRiskChangeResult> changes, string scanId, CancellationToken ct = default) => Task.CompletedTask;
|
||||
|
||||
public Task<IReadOnlyList<MaterialRiskChangeResult>> GetChangesForScanAsync(string scanId, CancellationToken ct = default) =>
|
||||
Task.FromResult<IReadOnlyList<MaterialRiskChangeResult>>(Array.Empty<MaterialRiskChangeResult>());
|
||||
|
||||
public Task<IReadOnlyList<MaterialRiskChangeResult>> GetChangesForFindingAsync(FindingKey findingKey, int limit = 10, CancellationToken ct = default) =>
|
||||
Task.FromResult<IReadOnlyList<MaterialRiskChangeResult>>(Array.Empty<MaterialRiskChangeResult>());
|
||||
|
||||
public Task<MaterialRiskChangeQueryResult> QueryChangesAsync(MaterialRiskChangeQuery query, CancellationToken ct = default) =>
|
||||
Task.FromResult(new MaterialRiskChangeQueryResult(ImmutableArray<MaterialRiskChangeResult>.Empty, 0, query.Offset, query.Limit));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user