Add unit tests for SBOM ingestion and transformation
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
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:
@@ -354,7 +354,7 @@ internal static class CommandHandlers
|
||||
throw new InvalidOperationException("Tenant must be provided via --tenant or STELLA_TENANT.");
|
||||
}
|
||||
|
||||
var payload = await LoadIngestInputAsync(input, cancellationToken).ConfigureAwait(false);
|
||||
var payload = await LoadIngestInputAsync(services, input, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
logger.LogInformation("Executing ingestion dry-run for source {Source} using input {Input}.", source, payload.Name);
|
||||
|
||||
@@ -5009,22 +5009,22 @@ internal static class CommandHandlers
|
||||
return string.IsNullOrWhiteSpace(fromEnvironment) ? string.Empty : fromEnvironment.Trim();
|
||||
}
|
||||
|
||||
private static async Task<IngestInputPayload> LoadIngestInputAsync(string input, CancellationToken cancellationToken)
|
||||
private static async Task<IngestInputPayload> LoadIngestInputAsync(IServiceProvider services, string input, CancellationToken cancellationToken)
|
||||
{
|
||||
if (Uri.TryCreate(input, UriKind.Absolute, out var uri) &&
|
||||
(uri.Scheme.Equals(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) ||
|
||||
uri.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return await LoadIngestInputFromHttpAsync(uri, cancellationToken).ConfigureAwait(false);
|
||||
return await LoadIngestInputFromHttpAsync(services, uri, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return await LoadIngestInputFromFileAsync(input, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static async Task<IngestInputPayload> LoadIngestInputFromHttpAsync(Uri uri, CancellationToken cancellationToken)
|
||||
private static async Task<IngestInputPayload> LoadIngestInputFromHttpAsync(IServiceProvider services, Uri uri, CancellationToken cancellationToken)
|
||||
{
|
||||
using var handler = new HttpClientHandler { AutomaticDecompression = DecompressionMethods.All };
|
||||
using var httpClient = new HttpClient(handler);
|
||||
var httpClientFactory = services.GetRequiredService<IHttpClientFactory>();
|
||||
var httpClient = httpClientFactory.CreateClient("stellaops-cli.ingest-download");
|
||||
using var response = await httpClient.GetAsync(uri, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.AirGap.Policy;
|
||||
|
||||
namespace StellaOps.Cli.Configuration;
|
||||
|
||||
internal sealed class EgressPolicyHttpMessageHandler : DelegatingHandler
|
||||
{
|
||||
private readonly IEgressPolicy? _policy;
|
||||
private readonly ILogger _logger;
|
||||
private readonly string _component;
|
||||
private readonly string _intent;
|
||||
|
||||
public EgressPolicyHttpMessageHandler(IEgressPolicy? policy, ILogger logger, string component, string intent)
|
||||
{
|
||||
_policy = policy;
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_component = string.IsNullOrWhiteSpace(component) ? "stellaops-cli" : component;
|
||||
_intent = string.IsNullOrWhiteSpace(intent) ? "cli-http" : intent;
|
||||
}
|
||||
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_policy is null || request.RequestUri is not { IsAbsoluteUri: true } uri)
|
||||
{
|
||||
return base.SendAsync(request, cancellationToken);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var egressRequest = new EgressRequest(
|
||||
_component,
|
||||
uri,
|
||||
_intent,
|
||||
operation: request.Method.Method);
|
||||
|
||||
_policy.EnsureAllowed(egressRequest);
|
||||
}
|
||||
catch (AirGapEgressBlockedException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Egress blocked for {Component} when contacting {Destination}", _component, request.RequestUri);
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
return base.SendAsync(request, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.AirGap.Policy;
|
||||
|
||||
namespace StellaOps.Cli.Configuration;
|
||||
|
||||
internal static class HttpClientBuilderExtensions
|
||||
{
|
||||
public static IHttpClientBuilder AddEgressPolicyGuard(this IHttpClientBuilder builder, string component, string intent)
|
||||
{
|
||||
if (builder is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
return builder.AddHttpMessageHandler(sp =>
|
||||
{
|
||||
var policy = sp.GetService<IEgressPolicy>();
|
||||
var loggerFactory = sp.GetRequiredService<ILoggerFactory>();
|
||||
return new EgressPolicyHttpMessageHandler(
|
||||
policy,
|
||||
loggerFactory.CreateLogger<EgressPolicyHttpMessageHandler>(),
|
||||
component,
|
||||
intent);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.CommandLine;
|
||||
using System;
|
||||
using System.CommandLine;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -9,7 +10,8 @@ using StellaOps.Auth.Client;
|
||||
using StellaOps.Cli.Commands;
|
||||
using StellaOps.Cli.Configuration;
|
||||
using StellaOps.Cli.Services;
|
||||
using StellaOps.Cli.Telemetry;
|
||||
using StellaOps.Cli.Telemetry;
|
||||
using StellaOps.AirGap.Policy;
|
||||
|
||||
namespace StellaOps.Cli;
|
||||
|
||||
@@ -24,7 +26,8 @@ internal static class Program
|
||||
services.AddSingleton(options);
|
||||
|
||||
var verbosityState = new VerbosityState();
|
||||
services.AddSingleton(verbosityState);
|
||||
services.AddSingleton(verbosityState);
|
||||
services.AddAirGapEgressPolicy(configuration);
|
||||
|
||||
services.AddLogging(builder =>
|
||||
{
|
||||
@@ -89,7 +92,7 @@ internal static class Program
|
||||
{
|
||||
client.BaseAddress = authorityUri;
|
||||
}
|
||||
});
|
||||
}).AddEgressPolicyGuard("stellaops-cli", "authority-revocation");
|
||||
}
|
||||
|
||||
services.AddHttpClient<IBackendOperationsClient, BackendOperationsClient>(client =>
|
||||
@@ -100,7 +103,7 @@ internal static class Program
|
||||
{
|
||||
client.BaseAddress = backendUri;
|
||||
}
|
||||
});
|
||||
}).AddEgressPolicyGuard("stellaops-cli", "backend-api");
|
||||
|
||||
services.AddHttpClient<IConcelierObservationsClient, ConcelierObservationsClient>(client =>
|
||||
{
|
||||
@@ -110,7 +113,14 @@ internal static class Program
|
||||
{
|
||||
client.BaseAddress = concelierUri;
|
||||
}
|
||||
});
|
||||
}).AddEgressPolicyGuard("stellaops-cli", "concelier-api");
|
||||
|
||||
services.AddHttpClient("stellaops-cli.ingest-download")
|
||||
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
|
||||
{
|
||||
AutomaticDecompression = DecompressionMethods.All
|
||||
})
|
||||
.AddEgressPolicyGuard("stellaops-cli", "sources-ingest");
|
||||
|
||||
services.AddSingleton<IScannerExecutor, ScannerExecutor>();
|
||||
services.AddSingleton<IScannerInstaller, ScannerInstaller>();
|
||||
@@ -127,8 +137,30 @@ internal static class Program
|
||||
};
|
||||
|
||||
var rootCommand = CommandFactory.Create(serviceProvider, options, cts.Token, loggerFactory);
|
||||
var commandConfiguration = new CommandLineConfiguration(rootCommand);
|
||||
var commandExit = await commandConfiguration.InvokeAsync(args, cts.Token).ConfigureAwait(false);
|
||||
var commandConfiguration = new CommandLineConfiguration(rootCommand);
|
||||
int commandExit;
|
||||
try
|
||||
{
|
||||
commandExit = await commandConfiguration.InvokeAsync(args, cts.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch (AirGapEgressBlockedException ex)
|
||||
{
|
||||
var guardLogger = loggerFactory.CreateLogger("StellaOps.Cli.AirGap");
|
||||
guardLogger.LogError("{ErrorCode}: {Reason} Remediation: {Remediation}", AirGapEgressBlockedException.ErrorCode, ex.Reason, ex.Remediation);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(ex.DocumentationUrl))
|
||||
{
|
||||
guardLogger.LogInformation("Documentation: {DocumentationUrl}", ex.DocumentationUrl);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(ex.SupportContact))
|
||||
{
|
||||
guardLogger.LogInformation("Support contact: {SupportContact}", ex.SupportContact);
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(ex.Message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
var finalExit = Environment.ExitCode != 0 ? Environment.ExitCode : commandExit;
|
||||
if (cts.IsCancellationRequested && finalExit == 0)
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Configuration/StellaOps.Configuration.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Cryptography.Kms/StellaOps.Cryptography.Kms.csproj" />
|
||||
<ProjectReference Include="../../AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.csproj" />
|
||||
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOps.Auth.Abstractions.csproj" />
|
||||
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.Client/StellaOps.Auth.Client.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Plugin/StellaOps.Plugin.csproj" />
|
||||
|
||||
@@ -12,6 +12,11 @@
|
||||
> 2025-10-27: CLI reference now reflects final summary fields/JSON schema, quickstart includes verification/dry-run workflows, and API reference tables list both `sources ingest --dry-run` and `aoc verify`.
|
||||
> 2025-11-01: Update CLI auth defaults to request `attestor.verify` (and `attestor.read` for list/detail) after Attestor scope split; tokens without new scopes will fail verification calls.
|
||||
|
||||
## Replay Enablement
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
| CLI-REPLAY-187-002 | TODO | DevEx/CLI Guild | REPLAY-CORE-185-001, SCAN-REPLAY-186-001 | Implement `scan --record`, `verify`, `replay`, and `diff` commands with offline bundle resolution; update `docs/modules/cli/architecture.md` appendix referencing `docs/replay/DEVS_GUIDE_REPLAY.md`. | Commands tested (unit/integration); docs merged; offline workflows validated with sample bundles. |
|
||||
|
||||
## Policy Engine v2
|
||||
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|
||||
@@ -2327,14 +2327,15 @@ public sealed class CommandHandlersTests
|
||||
IStellaOpsTokenClient? tokenClient = null,
|
||||
IConcelierObservationsClient? concelierClient = null)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton(backend);
|
||||
services.AddSingleton<ILoggerFactory>(_ => LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.Debug)));
|
||||
services.AddSingleton(new VerbosityState());
|
||||
var resolvedOptions = options ?? new StellaOpsCliOptions
|
||||
{
|
||||
ResultsDirectory = Path.Combine(Path.GetTempPath(), $"stellaops-cli-results-{Guid.NewGuid():N}")
|
||||
};
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton(backend);
|
||||
services.AddSingleton<ILoggerFactory>(_ => LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.Debug)));
|
||||
services.AddSingleton(new VerbosityState());
|
||||
services.AddHttpClient();
|
||||
var resolvedOptions = options ?? new StellaOpsCliOptions
|
||||
{
|
||||
ResultsDirectory = Path.Combine(Path.GetTempPath(), $"stellaops-cli-results-{Guid.NewGuid():N}")
|
||||
};
|
||||
services.AddSingleton(resolvedOptions);
|
||||
|
||||
var resolvedExecutor = executor ?? CreateDefaultExecutor();
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
using Xunit;
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.AirGap.Policy;
|
||||
using StellaOps.Cli.Configuration;
|
||||
|
||||
namespace StellaOps.Cli.Tests.Configuration;
|
||||
|
||||
public sealed class EgressPolicyHttpMessageHandlerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task SendAsync_AllowsRequestWhenPolicyPermits()
|
||||
{
|
||||
var options = new EgressPolicyOptions
|
||||
{
|
||||
Mode = EgressPolicyMode.Sealed
|
||||
};
|
||||
options.AddAllowRule(example.com);
|
||||
|
||||
var policy = new EgressPolicy(options);
|
||||
var handler = new EgressPolicyHttpMessageHandler(policy, NullLogger<EgressPolicyHttpMessageHandler>.Instance, cli, test)
|
||||
{
|
||||
InnerHandler = new StubHandler()
|
||||
};
|
||||
|
||||
var client = new HttpClient(handler, disposeHandler: true);
|
||||
var response = await client.GetAsync(https://example.com/resource, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendAsync_ThrowsWhenPolicyBlocksRequest()
|
||||
{
|
||||
var options = new EgressPolicyOptions
|
||||
{
|
||||
Mode = EgressPolicyMode.Sealed
|
||||
};
|
||||
|
||||
var policy = new EgressPolicy(options);
|
||||
var handler = new EgressPolicyHttpMessageHandler(policy, NullLogger<EgressPolicyHttpMessageHandler>.Instance, cli, test)
|
||||
{
|
||||
InnerHandler = new StubHandler()
|
||||
};
|
||||
|
||||
var client = new HttpClient(handler, disposeHandler: true);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<AirGapEgressBlockedException>(
|
||||
() => client.GetAsync(https://blocked.example, CancellationToken.None)).ConfigureAwait(false);
|
||||
|
||||
Assert.Contains(AirGapEgressBlockedException.ErrorCode, exception.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private sealed class StubHandler : HttpMessageHandler
|
||||
{
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
=> Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user