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:
@@ -0,0 +1,70 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Findings.Ledger.WebService.Contracts;
|
||||
|
||||
public sealed record LedgerEventRequest
|
||||
{
|
||||
[JsonPropertyName("tenantId")]
|
||||
public required string TenantId { get; init; }
|
||||
|
||||
[JsonPropertyName("chainId")]
|
||||
public required Guid ChainId { get; init; }
|
||||
|
||||
[JsonPropertyName("sequence")]
|
||||
public required long Sequence { get; init; }
|
||||
|
||||
[JsonPropertyName("eventId")]
|
||||
public required Guid EventId { get; init; }
|
||||
|
||||
[JsonPropertyName("eventType")]
|
||||
public required string EventType { get; init; }
|
||||
|
||||
[JsonPropertyName("policyVersion")]
|
||||
public required string PolicyVersion { get; init; }
|
||||
|
||||
[JsonPropertyName("finding")]
|
||||
public required LedgerFindingRequest Finding { get; init; }
|
||||
|
||||
[JsonPropertyName("artifactId")]
|
||||
public required string ArtifactId { get; init; }
|
||||
|
||||
[JsonPropertyName("sourceRunId")]
|
||||
public Guid? SourceRunId { get; init; }
|
||||
|
||||
[JsonPropertyName("actor")]
|
||||
public required LedgerActorRequest Actor { get; init; }
|
||||
|
||||
[JsonPropertyName("occurredAt")]
|
||||
public required DateTimeOffset OccurredAt { get; init; }
|
||||
|
||||
[JsonPropertyName("recordedAt")]
|
||||
public DateTimeOffset? RecordedAt { get; init; }
|
||||
|
||||
[JsonPropertyName("payload")]
|
||||
public JsonObject? Payload { get; init; }
|
||||
|
||||
[JsonPropertyName("previousHash")]
|
||||
public string? PreviousHash { get; init; }
|
||||
}
|
||||
|
||||
public sealed record LedgerFindingRequest
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public required string Id { get; init; }
|
||||
|
||||
[JsonPropertyName("artifactId")]
|
||||
public string? ArtifactId { get; init; }
|
||||
|
||||
[JsonPropertyName("vulnId")]
|
||||
public required string VulnId { get; init; }
|
||||
}
|
||||
|
||||
public sealed record LedgerActorRequest
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public required string Id { get; init; }
|
||||
|
||||
[JsonPropertyName("type")]
|
||||
public required string Type { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Findings.Ledger.WebService.Contracts;
|
||||
|
||||
public sealed record LedgerEventResponse
|
||||
{
|
||||
[JsonPropertyName("eventId")]
|
||||
public Guid EventId { get; init; }
|
||||
|
||||
[JsonPropertyName("chainId")]
|
||||
public Guid ChainId { get; init; }
|
||||
|
||||
[JsonPropertyName("sequence")]
|
||||
public long Sequence { get; init; }
|
||||
|
||||
[JsonPropertyName("status")]
|
||||
public string Status { get; init; } = "created";
|
||||
|
||||
[JsonPropertyName("eventHash")]
|
||||
public string EventHash { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("previousHash")]
|
||||
public string PreviousHash { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("merkleLeafHash")]
|
||||
public string MerkleLeafHash { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("recordedAt")]
|
||||
public DateTimeOffset RecordedAt { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using StellaOps.Findings.Ledger.Domain;
|
||||
using StellaOps.Findings.Ledger.WebService.Contracts;
|
||||
|
||||
namespace StellaOps.Findings.Ledger.WebService.Mappings;
|
||||
|
||||
public static class LedgerEventMapping
|
||||
{
|
||||
public static LedgerEventDraft ToDraft(this LedgerEventRequest request)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
var recordedAt = (request.RecordedAt ?? DateTimeOffset.UtcNow).ToUniversalTime();
|
||||
var payload = request.Payload is null ? new JsonObject() : (JsonObject)request.Payload.DeepClone();
|
||||
|
||||
var eventObject = new JsonObject
|
||||
{
|
||||
["id"] = request.EventId.ToString(),
|
||||
["type"] = request.EventType,
|
||||
["tenant"] = request.TenantId,
|
||||
["chainId"] = request.ChainId.ToString(),
|
||||
["sequence"] = request.Sequence,
|
||||
["policyVersion"] = request.PolicyVersion,
|
||||
["artifactId"] = request.ArtifactId,
|
||||
["finding"] = new JsonObject
|
||||
{
|
||||
["id"] = request.Finding.Id,
|
||||
["artifactId"] = request.Finding.ArtifactId ?? request.ArtifactId,
|
||||
["vulnId"] = request.Finding.VulnId
|
||||
},
|
||||
["actor"] = new JsonObject
|
||||
{
|
||||
["id"] = request.Actor.Id,
|
||||
["type"] = request.Actor.Type
|
||||
},
|
||||
["occurredAt"] = FormatTimestamp(request.OccurredAt),
|
||||
["recordedAt"] = FormatTimestamp(recordedAt),
|
||||
["payload"] = payload
|
||||
};
|
||||
|
||||
if (request.SourceRunId is Guid sourceRunId && sourceRunId != Guid.Empty)
|
||||
{
|
||||
eventObject["sourceRunId"] = sourceRunId.ToString();
|
||||
}
|
||||
|
||||
var envelope = new JsonObject
|
||||
{
|
||||
["event"] = eventObject
|
||||
};
|
||||
|
||||
return new LedgerEventDraft(
|
||||
request.TenantId,
|
||||
request.ChainId,
|
||||
request.Sequence,
|
||||
request.EventId,
|
||||
request.EventType,
|
||||
request.PolicyVersion,
|
||||
request.Finding.Id,
|
||||
request.ArtifactId,
|
||||
request.SourceRunId,
|
||||
request.Actor.Id,
|
||||
request.Actor.Type,
|
||||
request.OccurredAt.ToUniversalTime(),
|
||||
recordedAt,
|
||||
payload,
|
||||
envelope,
|
||||
request.PreviousHash);
|
||||
}
|
||||
|
||||
private static string FormatTimestamp(DateTimeOffset value)
|
||||
=> value.ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss.fff'Z'");
|
||||
}
|
||||
206
src/Findings/StellaOps.Findings.Ledger.WebService/Program.cs
Normal file
206
src/Findings/StellaOps.Findings.Ledger.WebService/Program.cs
Normal file
@@ -0,0 +1,206 @@
|
||||
using Microsoft.AspNetCore.Diagnostics;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using StellaOps.Configuration;
|
||||
using StellaOps.DependencyInjection;
|
||||
using StellaOps.Findings.Ledger.Domain;
|
||||
using StellaOps.Findings.Ledger.Infrastructure;
|
||||
using StellaOps.Findings.Ledger.Infrastructure.Merkle;
|
||||
using StellaOps.Findings.Ledger.Infrastructure.Postgres;
|
||||
using StellaOps.Findings.Ledger.Infrastructure.Projection;
|
||||
using StellaOps.Findings.Ledger.Infrastructure.Policy;
|
||||
using StellaOps.Findings.Ledger.Options;
|
||||
using StellaOps.Findings.Ledger.Services;
|
||||
using StellaOps.Findings.Ledger.WebService.Contracts;
|
||||
using StellaOps.Findings.Ledger.WebService.Mappings;
|
||||
using StellaOps.Telemetry.Core;
|
||||
|
||||
const string LedgerWritePolicy = "ledger.events.write";
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Configuration.AddStellaOpsDefaults(options =>
|
||||
{
|
||||
options.BasePath = builder.Environment.ContentRootPath;
|
||||
options.EnvironmentPrefix = "FINDINGS_LEDGER_";
|
||||
options.ConfigureBuilder = configurationBuilder =>
|
||||
{
|
||||
configurationBuilder.AddYamlFile("../etc/findings-ledger.yaml", optional: true, reloadOnChange: true);
|
||||
};
|
||||
});
|
||||
|
||||
var bootstrapOptions = builder.Configuration.BindOptions<LedgerServiceOptions>(
|
||||
LedgerServiceOptions.SectionName,
|
||||
(opts, _) => opts.Validate());
|
||||
|
||||
builder.Host.UseSerilog((context, services, loggerConfiguration) =>
|
||||
{
|
||||
loggerConfiguration
|
||||
.MinimumLevel.Information()
|
||||
.MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
|
||||
.Enrich.FromLogContext()
|
||||
.WriteTo.Console();
|
||||
});
|
||||
|
||||
builder.Services.AddOptions<LedgerServiceOptions>()
|
||||
.Bind(builder.Configuration.GetSection(LedgerServiceOptions.SectionName))
|
||||
.PostConfigure(options => options.Validate())
|
||||
.ValidateOnStart();
|
||||
|
||||
builder.Services.AddSingleton(TimeProvider.System);
|
||||
builder.Services.AddProblemDetails();
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddHealthChecks();
|
||||
|
||||
builder.Services.AddStellaOpsTelemetry(
|
||||
builder.Configuration,
|
||||
configureMetering: meterBuilder =>
|
||||
{
|
||||
meterBuilder.AddAspNetCoreInstrumentation();
|
||||
meterBuilder.AddHttpClientInstrumentation();
|
||||
},
|
||||
configureTracing: tracerBuilder =>
|
||||
{
|
||||
tracerBuilder.AddAspNetCoreInstrumentation();
|
||||
tracerBuilder.AddHttpClientInstrumentation();
|
||||
});
|
||||
|
||||
builder.Services.AddStellaOpsResourceServerAuthentication(
|
||||
builder.Configuration,
|
||||
configurationSection: null,
|
||||
configure: resourceOptions =>
|
||||
{
|
||||
resourceOptions.Authority = bootstrapOptions.Authority.Issuer;
|
||||
resourceOptions.RequireHttpsMetadata = bootstrapOptions.Authority.RequireHttpsMetadata;
|
||||
resourceOptions.MetadataAddress = bootstrapOptions.Authority.MetadataAddress;
|
||||
resourceOptions.BackchannelTimeout = bootstrapOptions.Authority.BackchannelTimeout;
|
||||
resourceOptions.TokenClockSkew = bootstrapOptions.Authority.TokenClockSkew;
|
||||
|
||||
resourceOptions.Audiences.Clear();
|
||||
foreach (var audience in bootstrapOptions.Authority.Audiences)
|
||||
{
|
||||
resourceOptions.Audiences.Add(audience);
|
||||
}
|
||||
|
||||
resourceOptions.RequiredScopes.Clear();
|
||||
foreach (var scope in bootstrapOptions.Authority.RequiredScopes)
|
||||
{
|
||||
resourceOptions.RequiredScopes.Add(scope);
|
||||
}
|
||||
|
||||
foreach (var network in bootstrapOptions.Authority.BypassNetworks)
|
||||
{
|
||||
resourceOptions.BypassNetworks.Add(network);
|
||||
}
|
||||
});
|
||||
|
||||
builder.Services.AddAuthorization(options =>
|
||||
{
|
||||
var scopes = bootstrapOptions.Authority.RequiredScopes.Count > 0
|
||||
? bootstrapOptions.Authority.RequiredScopes.ToArray()
|
||||
: new[] { StellaOpsScopes.VulnOperate };
|
||||
|
||||
options.AddPolicy(LedgerWritePolicy, policy =>
|
||||
{
|
||||
policy.RequireAuthenticatedUser();
|
||||
policy.Requirements.Add(new StellaOpsScopeRequirement(scopes));
|
||||
policy.AddAuthenticationSchemes(StellaOpsAuthenticationDefaults.AuthenticationScheme);
|
||||
});
|
||||
});
|
||||
|
||||
builder.Services.AddSingleton<LedgerAnchorQueue>();
|
||||
builder.Services.AddSingleton<LedgerDataSource>();
|
||||
builder.Services.AddSingleton<IMerkleAnchorRepository, PostgresMerkleAnchorRepository>();
|
||||
builder.Services.AddSingleton<ILedgerEventRepository, PostgresLedgerEventRepository>();
|
||||
builder.Services.AddSingleton<IMerkleAnchorScheduler, PostgresMerkleAnchorScheduler>();
|
||||
builder.Services.AddSingleton<ILedgerEventStream, PostgresLedgerEventStream>();
|
||||
builder.Services.AddSingleton<IFindingProjectionRepository, PostgresFindingProjectionRepository>();
|
||||
builder.Services.AddSingleton<IPolicyEvaluationService, InlinePolicyEvaluationService>();
|
||||
builder.Services.AddSingleton<ILedgerEventWriteService, LedgerEventWriteService>();
|
||||
builder.Services.AddHostedService<LedgerMerkleAnchorWorker>();
|
||||
builder.Services.AddHostedService<LedgerProjectionWorker>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseSerilogRequestLogging();
|
||||
app.UseExceptionHandler(exceptionApp =>
|
||||
{
|
||||
exceptionApp.Run(async context =>
|
||||
{
|
||||
var feature = context.Features.Get<IExceptionHandlerFeature>();
|
||||
if (feature?.Error is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var problem = Results.Problem(
|
||||
statusCode: StatusCodes.Status500InternalServerError,
|
||||
title: "ledger_internal_error",
|
||||
detail: feature.Error.Message);
|
||||
await problem.ExecuteAsync(context);
|
||||
});
|
||||
});
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapHealthChecks("/healthz");
|
||||
|
||||
app.MapPost("/vuln/ledger/events", async Task<Results<Created<LedgerEventResponse>, Ok<LedgerEventResponse>, ProblemHttpResult>> (
|
||||
LedgerEventRequest request,
|
||||
ILedgerEventWriteService writeService,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var draft = request.ToDraft();
|
||||
var result = await writeService.AppendAsync(draft, cancellationToken).ConfigureAwait(false);
|
||||
return result.Status switch
|
||||
{
|
||||
LedgerWriteStatus.Success => CreateCreatedResponse(result.Record!),
|
||||
LedgerWriteStatus.Idempotent => TypedResults.Ok(CreateResponse(result.Record!, "idempotent")),
|
||||
LedgerWriteStatus.ValidationFailed => TypedResults.Problem(
|
||||
statusCode: StatusCodes.Status400BadRequest,
|
||||
title: "validation_failed",
|
||||
detail: string.Join(";", result.Errors)),
|
||||
LedgerWriteStatus.Conflict => TypedResults.Problem(
|
||||
statusCode: StatusCodes.Status409Conflict,
|
||||
title: result.ConflictCode ?? "conflict",
|
||||
detail: string.Join(";", result.Errors)),
|
||||
_ => TypedResults.Problem(
|
||||
statusCode: StatusCodes.Status500InternalServerError,
|
||||
title: "ledger_internal_error",
|
||||
detail: "Unexpected ledger status.")
|
||||
};
|
||||
})
|
||||
.WithName("LedgerEventAppend")
|
||||
.RequireAuthorization(LedgerWritePolicy)
|
||||
.Produces(StatusCodes.Status201Created)
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest)
|
||||
.ProducesProblem(StatusCodes.Status409Conflict)
|
||||
.ProducesProblem(StatusCodes.Status500InternalServerError);
|
||||
|
||||
app.Run();
|
||||
|
||||
static Created<LedgerEventResponse> CreateCreatedResponse(LedgerEventRecord record)
|
||||
{
|
||||
var response = CreateResponse(record, "created");
|
||||
return TypedResults.Created($"/vuln/ledger/events/{record.EventId}", response);
|
||||
}
|
||||
|
||||
static LedgerEventResponse CreateResponse(LedgerEventRecord record, string status)
|
||||
=> new()
|
||||
{
|
||||
EventId = record.EventId,
|
||||
ChainId = record.ChainId,
|
||||
Sequence = record.SequenceNumber,
|
||||
Status = status,
|
||||
EventHash = record.EventHash,
|
||||
PreviousHash = record.PreviousHash,
|
||||
MerkleLeafHash = record.MerkleLeafHash,
|
||||
RecordedAt = record.RecordedAt
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Findings.Ledger\StellaOps.Findings.Ledger.csproj" />
|
||||
<ProjectReference Include="..\..\Authority\StellaOps.Authority\StellaOps.Auth.Abstractions\StellaOps.Auth.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\..\Authority\StellaOps.Authority\StellaOps.Auth.ServerIntegration\StellaOps.Auth.ServerIntegration.csproj" />
|
||||
<ProjectReference Include="..\..\AirGap\StellaOps.AirGap.Policy\StellaOps.AirGap.Policy\StellaOps.AirGap.Policy.csproj" />
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Configuration\StellaOps.Configuration.csproj" />
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.DependencyInjection\StellaOps.DependencyInjection.csproj" />
|
||||
<ProjectReference Include="..\..\Telemetry\StellaOps.Telemetry.Core\StellaOps.Telemetry.Core\StellaOps.Telemetry.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user