Add unit tests for SBOM ingestion and transformation
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.
This commit is contained in:
master
2025-11-04 07:49:39 +02:00
parent f72c5c513a
commit 2eb6852d34
491 changed files with 39445 additions and 3917 deletions

View File

@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageReference Include="coverlet.collector" Version="6.0.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\\StellaOps.Telemetry.Core\\StellaOps.Telemetry.Core.csproj" />
<ProjectReference Include="..\\..\\..\\AirGap\\StellaOps.AirGap.Policy\\StellaOps.AirGap.Policy\\StellaOps.AirGap.Policy.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
using StellaOps.AirGap.Policy;
using StellaOps.Telemetry.Core;
using Xunit;
namespace StellaOps.Telemetry.Core.Tests;
public sealed class TelemetryExporterGuardTests
{
[Fact]
public void AllowsExporterWhenPolicyMissing()
{
var loggerFactory = CreateLoggerFactory();
var guard = new TelemetryExporterGuard(loggerFactory.CreateLogger<TelemetryExporterGuard>());
var descriptor = new TelemetryServiceDescriptor("TestService", "1.0.0");
var collectorOptions = new StellaOpsTelemetryOptions.CollectorOptions
{
Component = "test-service",
Intent = "telemetry-export",
};
var allowed = guard.IsExporterAllowed(
descriptor,
collectorOptions,
TelemetrySignal.Traces,
new Uri("https://collector.internal"),
out var decision);
Assert.True(allowed);
Assert.Null(decision);
}
[Fact]
public void BlocksRemoteEndpointWhenSealed()
{
var policyOptions = new EgressPolicyOptions
{
Mode = EgressPolicyMode.Sealed,
AllowLoopback = true,
};
var policy = new EgressPolicy(policyOptions);
var provider = new CollectingLoggerProvider();
using var loggerFactory = LoggerFactory.Create(builder => builder.AddProvider(provider));
var guard = new TelemetryExporterGuard(loggerFactory.CreateLogger<TelemetryExporterGuard>(), policy);
var descriptor = new TelemetryServiceDescriptor("PolicyEngine", "1.2.3");
var collectorOptions = new StellaOpsTelemetryOptions.CollectorOptions
{
Component = "policy-engine",
Intent = "telemetry-export",
};
var allowed = guard.IsExporterAllowed(
descriptor,
collectorOptions,
TelemetrySignal.Metrics,
new Uri("https://telemetry.example.com"),
out var decision);
Assert.False(allowed);
Assert.NotNull(decision);
Assert.Contains(provider.Entries, entry => entry.Level == LogLevel.Warning && entry.Message.Contains("disabled", StringComparison.OrdinalIgnoreCase));
}
private static ILoggerFactory CreateLoggerFactory()
=> LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.Debug));
private sealed class CollectingLoggerProvider : ILoggerProvider
{
public List<(LogLevel Level, string Message)> Entries { get; } = new();
public ILogger CreateLogger(string categoryName) => new CollectingLogger(Entries);
public void Dispose()
{
}
private sealed class CollectingLogger : ILogger
{
private readonly List<(LogLevel Level, string Message)> _entries;
public CollectingLogger(List<(LogLevel Level, string Message)> entries)
{
_entries = entries;
}
public IDisposable BeginScope<TState>(TState state) => NullScope.Instance;
public bool IsEnabled(LogLevel logLevel) => true;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
_entries.Add((logLevel, formatter(state, exception)));
}
}
private sealed class NullScope : IDisposable
{
public static NullScope Instance { get; } = new();
public void Dispose()
{
}
}
}
}

View File

@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\\..\\..\\AirGap\\StellaOps.AirGap.Policy\\StellaOps.AirGap.Policy\\StellaOps.AirGap.Policy.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,82 @@
using System;
namespace StellaOps.Telemetry.Core;
/// <summary>
/// Options controlling how StellaOps services emit telemetry.
/// </summary>
public sealed class StellaOpsTelemetryOptions
{
/// <summary>
/// Gets telemetry collector-specific options.
/// </summary>
public CollectorOptions Collector { get; set; } = new();
/// <summary>
/// Options describing how the OTLP collector exporter should be configured.
/// </summary>
public sealed class CollectorOptions
{
/// <summary>
/// Gets or sets a value indicating whether the collector exporter is enabled.
/// </summary>
public bool Enabled { get; set; } = true;
/// <summary>
/// Gets or sets the collector endpoint (absolute URI).
/// </summary>
public string? Endpoint { get; set; }
/// <summary>
/// Gets or sets the OTLP protocol used when contacting the collector.
/// </summary>
public TelemetryCollectorProtocol Protocol { get; set; } = TelemetryCollectorProtocol.Grpc;
/// <summary>
/// Gets or sets the component identifier used when evaluating egress policy requests.
/// </summary>
public string Component { get; set; } = "telemetry";
/// <summary>
/// Gets or sets the intent label used when evaluating egress policy requests.
/// </summary>
public string Intent { get; set; } = "telemetry-export";
/// <summary>
/// Gets or sets a value indicating whether the exporter should be disabled when policy blocks the endpoint.
/// </summary>
public bool DisableOnViolation { get; set; } = true;
/// <summary>
/// Attempts to parse the configured endpoint into a <see cref="Uri"/>.
/// </summary>
/// <param name="endpoint">Resolved endpoint when parsing succeeded.</param>
/// <returns><c>true</c> when the endpoint was parsed successfully.</returns>
public bool TryGetEndpoint(out Uri? endpoint)
{
endpoint = null;
if (string.IsNullOrWhiteSpace(Endpoint))
{
return false;
}
return Uri.TryCreate(Endpoint.Trim(), UriKind.Absolute, out endpoint);
}
}
}
/// <summary>
/// Supported OTLP protocols when exporting telemetry to the collector.
/// </summary>
public enum TelemetryCollectorProtocol
{
/// <summary>
/// OTLP over gRPC.
/// </summary>
Grpc = 0,
/// <summary>
/// OTLP over HTTP/protobuf.
/// </summary>
HttpProtobuf = 1,
}

View File

@@ -0,0 +1,98 @@
using System;
using Microsoft.Extensions.Logging;
using StellaOps.AirGap.Policy;
namespace StellaOps.Telemetry.Core;
/// <summary>
/// Applies the air-gap egress policy to telemetry exporters.
/// </summary>
public sealed class TelemetryExporterGuard
{
private readonly IEgressPolicy? _egressPolicy;
private readonly ILogger<TelemetryExporterGuard> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="TelemetryExporterGuard"/> class.
/// </summary>
/// <param name="logger">Logger used to report enforcement results.</param>
/// <param name="egressPolicy">Optional air-gap egress policy.</param>
public TelemetryExporterGuard(ILogger<TelemetryExporterGuard> logger, IEgressPolicy? egressPolicy = null)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_egressPolicy = egressPolicy;
}
/// <summary>
/// Determines whether the configured exporter endpoint may be used.
/// </summary>
/// <param name="descriptor">Service descriptor.</param>
/// <param name="options">Collector options.</param>
/// <param name="signal">Signal the exporter targets.</param>
/// <param name="endpoint">Endpoint that will be contacted.</param>
/// <param name="decision">Decision returned by the policy (if evaluated).</param>
/// <returns><c>true</c> when the exporter may be used.</returns>
public bool IsExporterAllowed(
TelemetryServiceDescriptor descriptor,
StellaOpsTelemetryOptions.CollectorOptions options,
TelemetrySignal signal,
Uri endpoint,
out EgressDecision? decision)
{
decision = null;
if (_egressPolicy is null)
{
return true;
}
var component = string.IsNullOrWhiteSpace(options.Component)
? descriptor.ServiceName
: options.Component.Trim();
var intent = string.IsNullOrWhiteSpace(options.Intent)
? $"telemetry-{signal.ToString().ToLowerInvariant()}"
: options.Intent.Trim();
decision = _egressPolicy.Evaluate(
new EgressRequest(component, endpoint, intent, operation: $"{signal}-export"));
if (decision.IsAllowed)
{
return true;
}
EmitDenialLog(signal, endpoint, decision);
return false;
}
private void EmitDenialLog(TelemetrySignal signal, Uri endpoint, EgressDecision decision)
{
var reason = string.IsNullOrWhiteSpace(decision.Reason)
? "Destination blocked by egress policy."
: decision.Reason!;
var remediation = string.IsNullOrWhiteSpace(decision.Remediation)
? "Review airgap.egressAllowlist configuration before enabling remote telemetry exporters."
: decision.Remediation!;
if (_egressPolicy?.IsSealed == true)
{
_logger.LogWarning(
"Sealed mode telemetry exporter disabled for {Signal} endpoint {Endpoint}: {Reason} Remediation: {Remediation}",
signal,
endpoint,
reason,
remediation);
}
else
{
_logger.LogWarning(
"Telemetry exporter for {Signal} denied by egress policy for endpoint {Endpoint}: {Reason} Remediation: {Remediation}",
signal,
endpoint,
reason,
remediation);
}
}
}

View File

@@ -0,0 +1,174 @@
using System;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OpenTelemetry;
using OpenTelemetry.Exporter;
using OpenTelemetry.Logs;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
using StellaOps.AirGap.Policy;
namespace StellaOps.Telemetry.Core;
/// <summary>
/// Service collection extensions for configuring StellaOps telemetry.
/// </summary>
public static class TelemetryServiceCollectionExtensions
{
/// <summary>
/// Registers the StellaOps telemetry stack with sealed-mode enforcement.
/// </summary>
/// <param name="services">Service collection to mutate.</param>
/// <param name="configuration">Application configuration.</param>
/// <param name="serviceName">Service name advertised to OpenTelemetry.</param>
/// <param name="serviceVersion">Optional service version.</param>
/// <param name="configureOptions">Optional options mutator.</param>
/// <param name="configureMetrics">Optional additional metrics configuration.</param>
/// <param name="configureTracing">Optional additional tracing configuration.</param>
/// <returns>The <see cref="OpenTelemetryBuilder"/> for further chaining.</returns>
public static OpenTelemetryBuilder AddStellaOpsTelemetry(
this IServiceCollection services,
IConfiguration configuration,
string serviceName,
string? serviceVersion = null,
Action<StellaOpsTelemetryOptions>? configureOptions = null,
Action<MeterProviderBuilder>? configureMetrics = null,
Action<TracerProviderBuilder>? configureTracing = null)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(configuration);
ArgumentException.ThrowIfNullOrEmpty(serviceName);
services.AddOptions<StellaOpsTelemetryOptions>()
.Bind(configuration.GetSection("Telemetry"))
.Configure(options => configureOptions?.Invoke(options))
.PostConfigure(options =>
{
if (string.IsNullOrWhiteSpace(options.Collector.Component))
{
options.Collector.Component = serviceName;
}
});
services.TryAddSingleton(_ => new TelemetryServiceDescriptor(serviceName, serviceVersion));
services.TryAddSingleton<TelemetryExporterGuard>();
var builder = services.AddOpenTelemetry();
builder.ConfigureResource(resource => resource.AddService(serviceName, serviceVersion: serviceVersion));
builder.WithTracing();
builder.WithMetrics();
builder.WithLogging();
Action<MeterProviderBuilder> metricsSetup = configureMetrics ?? DefaultMetricsSetup;
Action<TracerProviderBuilder> tracingSetup = configureTracing ?? DefaultTracingSetup;
services.ConfigureOpenTelemetryTracerProvider((sp, tracerBuilder) =>
{
tracingSetup(tracerBuilder);
ConfigureCollectorExporter(sp, tracerBuilder, TelemetrySignal.Traces);
});
services.ConfigureOpenTelemetryMeterProvider((sp, meterBuilder) =>
{
metricsSetup(meterBuilder);
ConfigureCollectorExporter(sp, meterBuilder, TelemetrySignal.Metrics);
});
services.Configure<OpenTelemetryLoggerOptions>((sp, options) =>
{
var configure = BuildExporterConfiguration(sp, TelemetrySignal.Logs);
if (configure is not null)
{
options.AddOtlpExporter(configure);
}
});
return builder;
}
private static void DefaultMetricsSetup(MeterProviderBuilder builder)
{
builder.AddRuntimeInstrumentation();
}
private static void DefaultTracingSetup(TracerProviderBuilder builder)
{
builder.AddAspNetCoreInstrumentation();
builder.AddHttpClientInstrumentation();
}
private static void ConfigureCollectorExporter(
IServiceProvider serviceProvider,
TracerProviderBuilder tracerBuilder,
TelemetrySignal signal)
{
var configure = BuildExporterConfiguration(serviceProvider, signal);
if (configure is not null)
{
tracerBuilder.AddOtlpExporter(configure);
}
}
private static void ConfigureCollectorExporter(
IServiceProvider serviceProvider,
MeterProviderBuilder meterBuilder,
TelemetrySignal signal)
{
var configure = BuildExporterConfiguration(serviceProvider, signal);
if (configure is not null)
{
meterBuilder.AddOtlpExporter(configure);
}
}
private static Action<OtlpExporterOptions>? BuildExporterConfiguration(IServiceProvider serviceProvider, TelemetrySignal signal)
{
var options = serviceProvider.GetRequiredService<IOptions<StellaOpsTelemetryOptions>>().Value;
var collector = options.Collector;
if (!collector.Enabled)
{
return null;
}
if (!collector.TryGetEndpoint(out var endpoint) || endpoint is null)
{
serviceProvider.GetRequiredService<ILoggerFactory>()
.CreateLogger(nameof(TelemetryServiceCollectionExtensions))
.LogDebug("Telemetry collector endpoint not configured; {Signal} exporter disabled.", signal);
return null;
}
var descriptor = serviceProvider.GetRequiredService<TelemetryServiceDescriptor>();
var guard = serviceProvider.GetRequiredService<TelemetryExporterGuard>();
if (!guard.IsExporterAllowed(descriptor, collector, signal, endpoint, out _) &&
collector.DisableOnViolation)
{
return null;
}
var egressPolicy = serviceProvider.GetService<IEgressPolicy>();
return exporterOptions =>
{
exporterOptions.Endpoint = endpoint;
exporterOptions.Protocol = collector.Protocol switch
{
TelemetryCollectorProtocol.HttpProtobuf => OtlpExportProtocol.HttpProtobuf,
_ => OtlpExportProtocol.Grpc,
};
if (egressPolicy is not null)
{
exporterOptions.HttpClientFactory = () => EgressHttpClientFactory.Create(
egressPolicy,
new EgressRequest(
collector.Component,
endpoint,
collector.Intent,
operation: $"{signal}-export"));
}
};
}
}

View File

@@ -0,0 +1,6 @@
namespace StellaOps.Telemetry.Core;
/// <summary>
/// Describes the hosting service emitting telemetry.
/// </summary>
public sealed record TelemetryServiceDescriptor(string ServiceName, string? ServiceVersion);

View File

@@ -0,0 +1,22 @@
namespace StellaOps.Telemetry.Core;
/// <summary>
/// Telemetry signal classification used when applying exporter policy.
/// </summary>
public enum TelemetrySignal
{
/// <summary>
/// Metrics signal.
/// </summary>
Metrics,
/// <summary>
/// Traces signal.
/// </summary>
Traces,
/// <summary>
/// Logs signal.
/// </summary>
Logs,
}