Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Implement `SbomIngestServiceCollectionExtensionsTests` to verify the SBOM ingestion pipeline exports snapshots correctly. - Create `SbomIngestTransformerTests` to ensure the transformation produces expected nodes and edges, including deduplication of license nodes and normalization of timestamps. - Add `SbomSnapshotExporterTests` to test the export functionality for manifest, adjacency, nodes, and edges. - Introduce `VexOverlayTransformerTests` to validate the transformation of VEX nodes and edges. - Set up project file for the test project with necessary dependencies and configurations. - Include JSON fixture files for testing purposes.
195 lines
7.3 KiB
C#
195 lines
7.3 KiB
C#
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using FluentAssertions;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using StellaOps.Graph.Indexer.Ingestion.Sbom;
|
|
using Xunit;
|
|
|
|
namespace StellaOps.Graph.Indexer.Tests;
|
|
|
|
public sealed class SbomIngestProcessorTests
|
|
{
|
|
[Fact]
|
|
public async Task ProcessAsync_writes_batch_and_records_success_metrics()
|
|
{
|
|
var snapshot = CreateSnapshot();
|
|
var transformer = new SbomIngestTransformer();
|
|
var writer = new CaptureWriter();
|
|
var metrics = new CaptureMetrics();
|
|
var snapshotExporter = new CaptureSnapshotExporter();
|
|
var processor = new SbomIngestProcessor(transformer, writer, metrics, snapshotExporter, NullLogger<SbomIngestProcessor>.Instance);
|
|
|
|
await processor.ProcessAsync(snapshot, CancellationToken.None);
|
|
|
|
writer.LastBatch.Should().NotBeNull();
|
|
metrics.LastRecord.Should().NotBeNull();
|
|
metrics.LastRecord!.Success.Should().BeTrue();
|
|
metrics.LastRecord.NodeCount.Should().Be(writer.LastBatch!.Nodes.Length);
|
|
metrics.LastRecord.EdgeCount.Should().Be(writer.LastBatch!.Edges.Length);
|
|
snapshotExporter.LastSnapshot.Should().BeSameAs(snapshot);
|
|
snapshotExporter.LastBatch.Should().BeSameAs(writer.LastBatch);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProcessAsync_records_failure_when_writer_throws()
|
|
{
|
|
var snapshot = CreateSnapshot();
|
|
var transformer = new SbomIngestTransformer();
|
|
var writer = new CaptureWriter(shouldThrow: true);
|
|
var metrics = new CaptureMetrics();
|
|
var snapshotExporter = new CaptureSnapshotExporter();
|
|
var processor = new SbomIngestProcessor(transformer, writer, metrics, snapshotExporter, NullLogger<SbomIngestProcessor>.Instance);
|
|
|
|
var act = () => processor.ProcessAsync(snapshot, CancellationToken.None);
|
|
|
|
await act.Should().ThrowAsync<InvalidOperationException>();
|
|
metrics.LastRecord.Should().NotBeNull();
|
|
metrics.LastRecord!.Success.Should().BeFalse();
|
|
snapshotExporter.LastSnapshot.Should().BeNull();
|
|
snapshotExporter.LastBatch.Should().BeNull();
|
|
}
|
|
|
|
private static SbomSnapshot CreateSnapshot()
|
|
{
|
|
return new SbomSnapshot
|
|
{
|
|
Tenant = "tenant-alpha",
|
|
Source = "scanner.sbom.v1",
|
|
ArtifactDigest = "sha256:test-artifact",
|
|
SbomDigest = "sha256:test-sbom",
|
|
CollectedAt = DateTimeOffset.Parse("2025-10-30T12:00:00Z"),
|
|
EventOffset = 1000,
|
|
Artifact = new SbomArtifactMetadata
|
|
{
|
|
DisplayName = "registry.example.com/app:latest",
|
|
Environment = "prod",
|
|
Labels = new[] { "demo" },
|
|
OriginRegistry = "registry.example.com",
|
|
SupplyChainStage = "deploy"
|
|
},
|
|
Build = new SbomBuildMetadata
|
|
{
|
|
BuilderId = "builder://tekton/default",
|
|
BuildType = "https://slsa.dev/provenance/v1",
|
|
AttestationDigest = "sha256:attestation",
|
|
Source = "scanner.build.v1",
|
|
CollectedAt = DateTimeOffset.Parse("2025-10-30T12:00:05Z"),
|
|
EventOffset = 2000
|
|
},
|
|
Components = new[]
|
|
{
|
|
new SbomComponent
|
|
{
|
|
Purl = "pkg:nuget/Example.Primary@1.0.0",
|
|
Version = "1.0.0",
|
|
Ecosystem = "nuget",
|
|
Scope = "runtime",
|
|
License = new SbomLicense
|
|
{
|
|
Spdx = "MIT",
|
|
Name = "MIT License",
|
|
Classification = "permissive",
|
|
SourceDigest = "sha256:license001"
|
|
},
|
|
Usage = "direct",
|
|
DetectedBy = "sbom.analyzer.transformer",
|
|
LayerDigest = "sha256:layer",
|
|
EvidenceDigest = "sha256:evidence",
|
|
CollectedAt = DateTimeOffset.Parse("2025-10-30T12:00:01Z"),
|
|
EventOffset = 1201,
|
|
Source = "scanner.component.v1",
|
|
Files = new[]
|
|
{
|
|
new SbomComponentFile
|
|
{
|
|
Path = "/src/app/Program.cs",
|
|
ContentSha256 = "sha256:file",
|
|
LanguageHint = "csharp",
|
|
SizeBytes = 1024,
|
|
Scope = "build",
|
|
DetectedBy = "sbom.analyzer.transformer",
|
|
EvidenceDigest = "sha256:file-evidence",
|
|
CollectedAt = DateTimeOffset.Parse("2025-10-30T12:00:02Z"),
|
|
EventOffset = 1202,
|
|
Source = "scanner.layer.v1"
|
|
}
|
|
},
|
|
Dependencies = Array.Empty<SbomDependency>(),
|
|
SourceType = "inventory"
|
|
}
|
|
},
|
|
BaseArtifacts = new[]
|
|
{
|
|
new SbomBaseArtifact
|
|
{
|
|
ArtifactDigest = "sha256:base",
|
|
SbomDigest = "sha256:base-sbom",
|
|
DisplayName = "registry.example.com/base:2025.09",
|
|
Environment = "prod",
|
|
Labels = new[] { "base-image" },
|
|
OriginRegistry = "registry.example.com",
|
|
SupplyChainStage = "build",
|
|
CollectedAt = DateTimeOffset.Parse("2025-10-22T08:00:00Z"),
|
|
EventOffset = 800,
|
|
Source = "scanner.sbom.v1"
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
private sealed class CaptureWriter : IGraphDocumentWriter
|
|
{
|
|
private readonly bool _shouldThrow;
|
|
|
|
public CaptureWriter(bool shouldThrow = false)
|
|
{
|
|
_shouldThrow = shouldThrow;
|
|
}
|
|
|
|
public GraphBuildBatch? LastBatch { get; private set; }
|
|
|
|
public Task WriteAsync(GraphBuildBatch batch, CancellationToken cancellationToken)
|
|
{
|
|
LastBatch = batch;
|
|
|
|
if (_shouldThrow)
|
|
{
|
|
throw new InvalidOperationException("Simulated persistence failure");
|
|
}
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|
|
|
|
private sealed class CaptureMetrics : ISbomIngestMetrics
|
|
{
|
|
public MetricRecord? LastRecord { get; private set; }
|
|
|
|
public void RecordBatch(string source, string tenant, int nodeCount, int edgeCount, TimeSpan duration, bool success)
|
|
{
|
|
LastRecord = new MetricRecord(source, tenant, nodeCount, edgeCount, duration, success);
|
|
}
|
|
}
|
|
|
|
private sealed class CaptureSnapshotExporter : ISbomSnapshotExporter
|
|
{
|
|
public SbomSnapshot? LastSnapshot { get; private set; }
|
|
public GraphBuildBatch? LastBatch { get; private set; }
|
|
|
|
public Task ExportAsync(SbomSnapshot snapshot, GraphBuildBatch batch, CancellationToken cancellationToken)
|
|
{
|
|
LastSnapshot = snapshot;
|
|
LastBatch = batch;
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|
|
|
|
private sealed record MetricRecord(
|
|
string Source,
|
|
string Tenant,
|
|
int NodeCount,
|
|
int EdgeCount,
|
|
TimeSpan Duration,
|
|
bool Success);
|
|
}
|