using System.Net; using System.Net.Http.Json; using System.Text.Json; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using StellaOps.Scanner.Triage; using StellaOps.Scanner.Triage.Entities; using StellaOps.Scanner.WebService.Contracts; using Xunit; using StellaOps.TestKit; namespace StellaOps.Scanner.WebService.Tests; public sealed class FindingsEvidenceControllerTests { private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web); [Trait("Category", TestCategories.Unit)] [Fact] public async Task GetEvidence_ReturnsNotFound_WhenFindingMissing() { using var secrets = new TestSurfaceSecretsScope(); using var factory = new ScannerApplicationFactory().WithOverrides(configuration => { configuration["scanner:authority:enabled"] = "false"; }); using var client = factory.CreateClient(); var response = await client.GetAsync($"/api/v1/findings/{Guid.NewGuid()}/evidence"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task GetEvidence_ReturnsForbidden_WhenRawScopeMissing() { using var secrets = new TestSurfaceSecretsScope(); using var factory = new ScannerApplicationFactory().WithOverrides(configuration => { configuration["scanner:authority:enabled"] = "false"; }); using var client = factory.CreateClient(); var response = await client.GetAsync($"/api/v1/findings/{Guid.NewGuid()}/evidence?includeRaw=true"); Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task GetEvidence_ReturnsEvidence_WhenFindingExists() { using var secrets = new TestSurfaceSecretsScope(); using var factory = new ScannerApplicationFactory().WithOverrides(configuration => { configuration["scanner:authority:enabled"] = "false"; }); using var client = factory.CreateClient(); var findingId = await SeedFindingAsync(factory); var response = await client.GetAsync($"/api/v1/findings/{findingId}/evidence"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var result = await response.Content.ReadFromJsonAsync(SerializerOptions); Assert.NotNull(result); Assert.Equal(findingId.ToString(), result!.FindingId); Assert.Equal("CVE-2024-12345", result.Cve); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task BatchEvidence_ReturnsBadRequest_WhenTooMany() { using var secrets = new TestSurfaceSecretsScope(); using var factory = new ScannerApplicationFactory().WithOverrides(configuration => { configuration["scanner:authority:enabled"] = "false"; }); using var client = factory.CreateClient(); var request = new BatchEvidenceRequest { FindingIds = Enumerable.Range(0, 101).Select(_ => Guid.NewGuid().ToString()).ToList() }; var response = await client.PostAsJsonAsync("/api/v1/findings/evidence/batch", request); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task BatchEvidence_ReturnsResults_ForExistingFindings() { using var secrets = new TestSurfaceSecretsScope(); using var factory = new ScannerApplicationFactory().WithOverrides(configuration => { configuration["scanner:authority:enabled"] = "false"; }); using var client = factory.CreateClient(); var findingId = await SeedFindingAsync(factory); var request = new BatchEvidenceRequest { FindingIds = new[] { findingId.ToString(), Guid.NewGuid().ToString() } }; var response = await client.PostAsJsonAsync("/api/v1/findings/evidence/batch", request); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var result = await response.Content.ReadFromJsonAsync(SerializerOptions); Assert.NotNull(result); Assert.Single(result!.Findings); Assert.Equal(findingId.ToString(), result.Findings[0].FindingId); } private static async Task SeedFindingAsync(ScannerApplicationFactory factory) { using var scope = factory.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); await db.Database.MigrateAsync(); var findingId = Guid.NewGuid(); var finding = new TriageFinding { Id = findingId, AssetId = Guid.NewGuid(), AssetLabel = "prod/api-gateway:1.2.3", Purl = "pkg:npm/lodash@4.17.20", CveId = "CVE-2024-12345", LastSeenAt = DateTimeOffset.UtcNow }; db.Findings.Add(finding); db.RiskResults.Add(new TriageRiskResult { FindingId = findingId, PolicyId = "policy-1", PolicyVersion = "1.0.0", InputsHash = "sha256:inputs", Score = 72, Verdict = TriageVerdict.Block, Lane = TriageLane.Blocked, Why = "High risk score", ComputedAt = DateTimeOffset.UtcNow }); db.EvidenceArtifacts.Add(new TriageEvidenceArtifact { FindingId = findingId, Type = TriageEvidenceType.Provenance, Title = "SBOM attestation", ContentHash = "sha256:attestation", Uri = "s3://evidence/attestation.json" }); await db.SaveChangesAsync(); return findingId; } }