using System.Net; using System.Net.Http.Json; using System.Text.Json; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Moq; using StellaOps.Scanner.Triage; using StellaOps.Scanner.Triage.Entities; using StellaOps.Scanner.WebService.Contracts; using StellaOps.Scanner.WebService.Services; 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(); var mockTriageService = new Mock(); mockTriageService.Setup(s => s.GetFindingAsync(It.IsAny(), It.IsAny())) .ReturnsAsync((TriageFinding?)null); await using var factory = new ScannerApplicationFactory().WithOverrides( configuration => { configuration["scanner:authority:enabled"] = "false"; }, configureServices: services => { services.RemoveAll(); services.AddSingleton(mockTriageService.Object); }); await factory.InitializeAsync(); 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(); await using var factory = new ScannerApplicationFactory().WithOverrides( configuration => { configuration["scanner:authority:enabled"] = "false"; }); await factory.InitializeAsync(); 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(); var findingId = Guid.NewGuid(); var now = DateTimeOffset.UtcNow; 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", FirstSeenAt = now, LastSeenAt = now, UpdatedAt = now }; var mockTriageService = new Mock(); mockTriageService.Setup(s => s.GetFindingAsync(findingId.ToString(), It.IsAny())) .ReturnsAsync(finding); var mockEvidenceService = new Mock(); mockEvidenceService.Setup(s => s.ComposeAsync(It.IsAny(), false, It.IsAny())) .ReturnsAsync(new FindingEvidenceResponse { FindingId = findingId.ToString(), Cve = "CVE-2024-12345", Component = new ComponentInfo { Name = "lodash", Version = "4.17.20", Purl = "pkg:npm/lodash@4.17.20" }, LastSeen = now }); await using var factory = new ScannerApplicationFactory().WithOverrides( configuration => { configuration["scanner:authority:enabled"] = "false"; }, configureServices: services => { services.RemoveAll(); services.AddSingleton(mockTriageService.Object); services.RemoveAll(); services.AddSingleton(mockEvidenceService.Object); }); await factory.InitializeAsync(); using var client = factory.CreateClient(); 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(); await using var factory = new ScannerApplicationFactory().WithOverrides( configuration => { configuration["scanner:authority:enabled"] = "false"; }); await factory.InitializeAsync(); 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(); var findingId = Guid.NewGuid(); var now = DateTimeOffset.UtcNow; 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", FirstSeenAt = now, LastSeenAt = now, UpdatedAt = now }; var mockTriageService = new Mock(); mockTriageService.Setup(s => s.GetFindingAsync(findingId.ToString(), It.IsAny())) .ReturnsAsync(finding); mockTriageService.Setup(s => s.GetFindingAsync(It.Is(id => id != findingId.ToString()), It.IsAny())) .ReturnsAsync((TriageFinding?)null); var mockEvidenceService = new Mock(); mockEvidenceService.Setup(s => s.ComposeAsync(It.IsAny(), false, It.IsAny())) .ReturnsAsync(new FindingEvidenceResponse { FindingId = findingId.ToString(), Cve = "CVE-2024-12345", Component = new ComponentInfo { Name = "lodash", Version = "4.17.20", Purl = "pkg:npm/lodash@4.17.20" }, LastSeen = now }); await using var factory = new ScannerApplicationFactory().WithOverrides( configuration => { configuration["scanner:authority:enabled"] = "false"; }, configureServices: services => { services.RemoveAll(); services.AddSingleton(mockTriageService.Object); services.RemoveAll(); services.AddSingleton(mockEvidenceService.Object); }); await factory.InitializeAsync(); using var client = factory.CreateClient(); 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); } }