save checkpoint
This commit is contained in:
@@ -1,83 +1,166 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using StellaOps.Scanner.Storage.ObjectStore;
|
||||
using StellaOps.Scanner.WebService.Contracts;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scanner.WebService.Tests;
|
||||
|
||||
public sealed class SbomUploadEndpointsTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Upload_validates_cyclonedx_format()
|
||||
public async Task Upload_accepts_cyclonedx_inline_and_persists_record()
|
||||
{
|
||||
// This test validates that CycloneDX format detection works
|
||||
// Full integration with upload service is tested separately
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
await using var factory = await CreateFactoryAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var sampleCycloneDx = """
|
||||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.6",
|
||||
"version": 1,
|
||||
"metadata": { "timestamp": "2025-01-15T10:00:00Z" },
|
||||
"components": []
|
||||
}
|
||||
""";
|
||||
var base64 = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(sampleCycloneDx));
|
||||
|
||||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.6",
|
||||
"version": 1,
|
||||
"components": [
|
||||
{
|
||||
"type": "library",
|
||||
"name": "left-pad",
|
||||
"version": "1.3.0",
|
||||
"purl": "pkg:npm/left-pad@1.3.0",
|
||||
"licenses": [{ "license": { "id": "MIT" } }]
|
||||
},
|
||||
{
|
||||
"type": "library",
|
||||
"name": "chalk",
|
||||
"version": "5.0.0",
|
||||
"purl": "pkg:npm/chalk@5.0.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
var request = new SbomUploadRequestDto
|
||||
{
|
||||
ArtifactRef = "example.com/app:1.0",
|
||||
SbomBase64 = base64,
|
||||
ArtifactRef = "example.com/app:1.0.0",
|
||||
ArtifactDigest = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
Sbom = JsonDocument.Parse(sampleCycloneDx).RootElement.Clone(),
|
||||
Source = new SbomUploadSourceDto
|
||||
{
|
||||
Tool = "syft",
|
||||
Version = "1.0.0"
|
||||
Version = "1.0.0",
|
||||
CiContext = new SbomUploadCiContextDto
|
||||
{
|
||||
BuildId = "build-42",
|
||||
Repository = "example/repo"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Verify the request is valid and can be serialized
|
||||
Assert.NotNull(request.ArtifactRef);
|
||||
Assert.NotEmpty(request.SbomBase64);
|
||||
Assert.NotNull(request.Source);
|
||||
Assert.Equal("syft", request.Source.Tool);
|
||||
var uploadResponse = await client.PostAsJsonAsync("/api/v1/sbom/upload", request);
|
||||
Assert.Equal(HttpStatusCode.Accepted, uploadResponse.StatusCode);
|
||||
|
||||
var payload = await uploadResponse.Content.ReadFromJsonAsync<SbomUploadResponseDto>();
|
||||
Assert.NotNull(payload);
|
||||
Assert.False(string.IsNullOrWhiteSpace(payload!.SbomId));
|
||||
Assert.Equal("cyclonedx", payload.Format);
|
||||
Assert.Equal("1.6", payload.FormatVersion);
|
||||
Assert.True(payload.ValidationResult.Valid);
|
||||
Assert.Equal(2, payload.ValidationResult.ComponentCount);
|
||||
Assert.Equal(0.85d, payload.ValidationResult.QualityScore);
|
||||
Assert.StartsWith("sha256:", payload.Digest, StringComparison.Ordinal);
|
||||
Assert.False(string.IsNullOrWhiteSpace(payload.AnalysisJobId));
|
||||
|
||||
var getResponse = await client.GetAsync($"/api/v1/sbom/uploads/{payload.SbomId}");
|
||||
Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode);
|
||||
|
||||
var record = await getResponse.Content.ReadFromJsonAsync<SbomUploadRecordDto>();
|
||||
Assert.NotNull(record);
|
||||
Assert.Equal(payload.SbomId, record!.SbomId);
|
||||
Assert.Equal("example.com/app:1.0.0", record.ArtifactRef);
|
||||
Assert.Equal("sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", record.ArtifactDigest);
|
||||
Assert.Equal("cyclonedx", record.Format);
|
||||
Assert.Equal("1.6", record.FormatVersion);
|
||||
Assert.Equal("build-42", record.Source?.CiContext?.BuildId);
|
||||
Assert.Equal("example/repo", record.Source?.CiContext?.Repository);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Upload_validates_spdx_format()
|
||||
public async Task Upload_accepts_spdx_base64_and_tracks_ci_context()
|
||||
{
|
||||
// This test validates that SPDX format detection works
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
await using var factory = await CreateFactoryAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var sampleSpdx = """
|
||||
{
|
||||
"spdxVersion": "SPDX-2.3",
|
||||
"dataLicense": "CC0-1.0",
|
||||
"SPDXID": "SPDXRef-DOCUMENT",
|
||||
"name": "test-sbom",
|
||||
"documentNamespace": "https://example.com/test",
|
||||
"packages": []
|
||||
}
|
||||
""";
|
||||
var base64 = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(sampleSpdx));
|
||||
{
|
||||
"spdxVersion": "SPDX-2.3",
|
||||
"dataLicense": "CC0-1.0",
|
||||
"SPDXID": "SPDXRef-DOCUMENT",
|
||||
"name": "test-sbom",
|
||||
"documentNamespace": "https://example.com/test",
|
||||
"packages": [
|
||||
{
|
||||
"name": "openssl",
|
||||
"versionInfo": "3.0.0",
|
||||
"licenseDeclared": "Apache-2.0",
|
||||
"externalRefs": [
|
||||
{
|
||||
"referenceType": "purl",
|
||||
"referenceLocator": "pkg:generic/openssl@3.0.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
var request = new SbomUploadRequestDto
|
||||
{
|
||||
ArtifactRef = "example.com/service:2.0",
|
||||
SbomBase64 = base64
|
||||
ArtifactRef = "example.com/service:2.0.0",
|
||||
SbomBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(sampleSpdx)),
|
||||
Source = new SbomUploadSourceDto
|
||||
{
|
||||
Tool = "trivy",
|
||||
Version = "0.50.0",
|
||||
CiContext = new SbomUploadCiContextDto
|
||||
{
|
||||
BuildId = "build-77",
|
||||
Repository = "example/service-repo"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Verify the request is valid
|
||||
Assert.NotNull(request.ArtifactRef);
|
||||
Assert.NotEmpty(request.SbomBase64);
|
||||
var uploadResponse = await client.PostAsJsonAsync("/api/v1/sbom/upload", request);
|
||||
Assert.Equal(HttpStatusCode.Accepted, uploadResponse.StatusCode);
|
||||
|
||||
var payload = await uploadResponse.Content.ReadFromJsonAsync<SbomUploadResponseDto>();
|
||||
Assert.NotNull(payload);
|
||||
Assert.Equal("spdx", payload!.Format);
|
||||
Assert.Equal("2.3", payload.FormatVersion);
|
||||
Assert.True(payload.ValidationResult.Valid);
|
||||
Assert.Equal(1, payload.ValidationResult.ComponentCount);
|
||||
Assert.Equal(1.0d, payload.ValidationResult.QualityScore);
|
||||
Assert.False(string.IsNullOrWhiteSpace(payload.AnalysisJobId));
|
||||
Assert.StartsWith("sha256:", payload.Digest, StringComparison.Ordinal);
|
||||
|
||||
var getResponse = await client.GetAsync($"/api/v1/sbom/uploads/{payload.SbomId}");
|
||||
Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode);
|
||||
|
||||
var record = await getResponse.Content.ReadFromJsonAsync<SbomUploadRecordDto>();
|
||||
Assert.NotNull(record);
|
||||
Assert.Equal("build-77", record!.Source?.CiContext?.BuildId);
|
||||
Assert.Equal("example/service-repo", record.Source?.CiContext?.Repository);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
[Fact]
|
||||
public async Task Upload_rejects_unknown_format()
|
||||
{
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
@@ -87,7 +170,7 @@ public sealed class SbomUploadEndpointsTests
|
||||
var invalid = new SbomUploadRequestDto
|
||||
{
|
||||
ArtifactRef = "example.com/invalid:1.0",
|
||||
SbomBase64 = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("{\"name\":\"oops\"}"))
|
||||
SbomBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes("{\"name\":\"oops\"}"))
|
||||
};
|
||||
|
||||
var response = await client.PostAsJsonAsync("/api/v1/sbom/upload", invalid);
|
||||
@@ -109,38 +192,6 @@ public sealed class SbomUploadEndpointsTests
|
||||
return factory;
|
||||
}
|
||||
|
||||
private static string LoadFixtureBase64(string fileName)
|
||||
{
|
||||
var repoRoot = ResolveRepoRoot();
|
||||
var path = Path.Combine(
|
||||
repoRoot,
|
||||
"src",
|
||||
"AirGap",
|
||||
"__Tests",
|
||||
"StellaOps.AirGap.Importer.Tests",
|
||||
"Reconciliation",
|
||||
"Fixtures",
|
||||
fileName);
|
||||
|
||||
Assert.True(File.Exists(path), $"Fixture not found at {path}.");
|
||||
var bytes = File.ReadAllBytes(path);
|
||||
return Convert.ToBase64String(bytes);
|
||||
}
|
||||
|
||||
private static string ResolveRepoRoot()
|
||||
{
|
||||
var baseDirectory = AppContext.BaseDirectory;
|
||||
return Path.GetFullPath(Path.Combine(
|
||||
baseDirectory,
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
".."));
|
||||
}
|
||||
|
||||
private sealed class InMemoryArtifactObjectStore : IArtifactObjectStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, byte[]> _objects = new(StringComparer.Ordinal);
|
||||
|
||||
@@ -153,7 +153,28 @@ public sealed partial class ScansEndpointsTests
|
||||
ImmutableArray<EntryTraceEdge>.Empty,
|
||||
ImmutableArray<EntryTraceDiagnostic>.Empty,
|
||||
ImmutableArray.Create(plan),
|
||||
ImmutableArray.Create(terminal));
|
||||
ImmutableArray.Create(terminal),
|
||||
new EntryTraceBinaryIntelligence(
|
||||
ImmutableArray.Create(new EntryTraceBinaryTarget(
|
||||
"/usr/local/bin/app",
|
||||
"sha256:abc",
|
||||
"X64",
|
||||
"ELF",
|
||||
3,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
ImmutableArray.Create(new EntryTraceBinaryVulnerability(
|
||||
"CVE-2024-1234",
|
||||
"SSL_read",
|
||||
"pkg:generic/openssl",
|
||||
"SSL_read",
|
||||
0.98f,
|
||||
"Critical")))),
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
generatedAt));
|
||||
|
||||
var ndjson = EntryTraceNdjsonWriter.Serialize(graph, new EntryTraceNdjsonMetadata(scanId, "sha256:test", generatedAt));
|
||||
var storedResult = new EntryTraceResult(scanId, "sha256:test", generatedAt, graph, ndjson);
|
||||
@@ -173,6 +194,8 @@ public sealed partial class ScansEndpointsTests
|
||||
Assert.Equal(storedResult.ScanId, payload!.ScanId);
|
||||
Assert.Equal(storedResult.ImageDigest, payload.ImageDigest);
|
||||
Assert.Equal(storedResult.Graph.Plans.Length, payload.Graph.Plans.Length);
|
||||
Assert.NotNull(payload.Graph.BinaryIntelligence);
|
||||
Assert.Equal(1, payload.Graph.BinaryIntelligence!.TotalVulnerableMatches);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
|
||||
@@ -4,6 +4,7 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| QA-SCANNER-VERIFY-008 | DONE | `SPRINT_20260212_002_Scanner_unchecked_feature_verification_batch1.md`: extended entry-trace endpoint contract test assertions for `graph.binaryIntelligence`; verified in run-002 Tier 2 (2026-02-12). |
|
||||
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/StellaOps.Scanner.WebService.Tests.md. |
|
||||
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
|
||||
| SPRINT-20260208-062-VEXREACH-001 | DONE | Added deterministic unit coverage for VEX+reachability filter matrix and controller endpoint (`6` tests passed on filtered run, 2026-02-08). |
|
||||
|
||||
Reference in New Issue
Block a user