Add new features and tests for AirGap and Time modules
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Introduced `SbomService` tasks documentation. - Updated `StellaOps.sln` to include new projects: `StellaOps.AirGap.Time` and `StellaOps.AirGap.Importer`. - Added unit tests for `BundleImportPlanner`, `DsseVerifier`, `ImportValidator`, and other components in the `StellaOps.AirGap.Importer.Tests` namespace. - Implemented `InMemoryBundleRepositories` for testing bundle catalog and item repositories. - Created `MerkleRootCalculator`, `RootRotationPolicy`, and `TufMetadataValidator` tests. - Developed `StalenessCalculator` and `TimeAnchorLoader` tests in the `StellaOps.AirGap.Time.Tests` namespace. - Added `fetch-sbomservice-deps.sh` script for offline dependency fetching.
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Concelier.WebService.Contracts;
|
||||
|
||||
public sealed record LnmLinksetResponse(
|
||||
[property: JsonPropertyName("advisoryId")] string AdvisoryId,
|
||||
[property: JsonPropertyName("source")] string Source,
|
||||
[property: JsonPropertyName("observations")] IReadOnlyList<string> Observations,
|
||||
[property: JsonPropertyName("normalized")] LnmLinksetNormalized? Normalized,
|
||||
[property: JsonPropertyName("conflicts")] IReadOnlyList<LnmLinksetConflict>? Conflicts,
|
||||
[property: JsonPropertyName("provenance")] LnmLinksetProvenance? Provenance,
|
||||
[property: JsonPropertyName("createdAt")] DateTimeOffset CreatedAt,
|
||||
[property: JsonPropertyName("builtByJobId")] string? BuiltByJobId,
|
||||
[property: JsonPropertyName("cached")] bool Cached);
|
||||
|
||||
public sealed record LnmLinksetNormalized(
|
||||
[property: JsonPropertyName("purls")] IReadOnlyList<string>? Purls,
|
||||
[property: JsonPropertyName("versions")] IReadOnlyList<string>? Versions,
|
||||
[property: JsonPropertyName("ranges")] IReadOnlyList<object>? Ranges,
|
||||
[property: JsonPropertyName("severities")] IReadOnlyList<object>? Severities);
|
||||
|
||||
public sealed record LnmLinksetConflict(
|
||||
[property: JsonPropertyName("field")] string Field,
|
||||
[property: JsonPropertyName("reason")] string Reason,
|
||||
[property: JsonPropertyName("values")] IReadOnlyList<string>? Values);
|
||||
|
||||
public sealed record LnmLinksetProvenance(
|
||||
[property: JsonPropertyName("observationHashes")] IReadOnlyList<string>? ObservationHashes,
|
||||
[property: JsonPropertyName("toolVersion")] string? ToolVersion,
|
||||
[property: JsonPropertyName("policyHash")] string? PolicyHash);
|
||||
|
||||
public sealed record LnmLinksetQuery(
|
||||
[Required]
|
||||
[property: JsonPropertyName("advisoryId")] string AdvisoryId,
|
||||
[Required]
|
||||
[property: JsonPropertyName("source")] string Source,
|
||||
[property: JsonPropertyName("includeConflicts")] bool IncludeConflicts = true);
|
||||
@@ -117,6 +117,8 @@ builder.Services.AddOptions<AdvisoryObservationEventPublisherOptions>()
|
||||
.ValidateOnStart();
|
||||
builder.Services.AddConcelierAocGuards();
|
||||
builder.Services.AddConcelierLinksetMappers();
|
||||
builder.Services.AddSingleton<IMeterFactory>(MeterProvider.Default.GetMeterProvider());
|
||||
builder.Services.AddSingleton<LinksetCacheTelemetry>();
|
||||
builder.Services.AddAdvisoryRawServices();
|
||||
builder.Services.AddSingleton<IAdvisoryObservationQueryService, AdvisoryObservationQueryService>();
|
||||
builder.Services.AddSingleton<AdvisoryChunkBuilder>();
|
||||
@@ -460,6 +462,66 @@ var observationsEndpoint = app.MapGet("/concelier/observations", async (
|
||||
return Results.Ok(response);
|
||||
}).WithName("GetConcelierObservations");
|
||||
|
||||
app.MapGet("/v1/lnm/linksets/{advisoryId}", async (
|
||||
HttpContext context,
|
||||
string advisoryId,
|
||||
[FromQuery(Name = "source")] string source,
|
||||
[FromQuery(Name = "includeConflicts")] bool includeConflicts,
|
||||
[FromServices] IAdvisoryLinksetLookup linksetLookup,
|
||||
[FromServices] LinksetCacheTelemetry telemetry,
|
||||
TimeProvider timeProvider,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
ApplyNoCache(context.Response);
|
||||
|
||||
if (!TryResolveTenant(context, requireHeader: true, out var tenant, out var tenantError))
|
||||
{
|
||||
return tenantError;
|
||||
}
|
||||
|
||||
var authorizationError = EnsureTenantAuthorized(context, tenant);
|
||||
if (authorizationError is not null)
|
||||
{
|
||||
return authorizationError;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(advisoryId) || string.IsNullOrWhiteSpace(source))
|
||||
{
|
||||
return Results.BadRequest("advisoryId and source are required.");
|
||||
}
|
||||
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
|
||||
var options = new AdvisoryLinksetQueryOptions(tenant!, Source: source.Trim(), AdvisoryId: advisoryId.Trim());
|
||||
var linksets = await linksetLookup.FindByTenantAsync(options.TenantId, options.Source, options.AdvisoryId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (linksets.Count == 0)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
var linkset = linksets[0];
|
||||
var response = new LnmLinksetResponse(
|
||||
linkset.AdvisoryId,
|
||||
linkset.Source,
|
||||
linkset.Observations,
|
||||
linkset.Normalized is null
|
||||
? null
|
||||
: new LnmLinksetNormalized(linkset.Normalized.Purls, linkset.Normalized.Versions, linkset.Normalized.Ranges, linkset.Normalized.Severities),
|
||||
includeConflicts ? linkset.Conflicts : Array.Empty<LnmLinksetConflict>(),
|
||||
linkset.Provenance is null
|
||||
? null
|
||||
: new LnmLinksetProvenance(linkset.Provenance.ObservationHashes, linkset.Provenance.ToolVersion, linkset.Provenance.PolicyHash),
|
||||
linkset.CreatedAt,
|
||||
linkset.BuiltByJobId,
|
||||
Cached: true);
|
||||
|
||||
telemetry.RecordHit(tenant, linkset.Source);
|
||||
telemetry.RecordRebuild(tenant, linkset.Source, stopwatch.Elapsed.TotalMilliseconds);
|
||||
|
||||
return Results.Ok(response);
|
||||
}).WithName("GetLnmLinkset");
|
||||
|
||||
if (authorityConfigured)
|
||||
{
|
||||
observationsEndpoint.RequireAuthorization(ObservationsPolicyName);
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
using System.Diagnostics.Metrics;
|
||||
|
||||
namespace StellaOps.Concelier.WebService.Telemetry;
|
||||
|
||||
internal sealed class LinksetCacheTelemetry
|
||||
{
|
||||
private readonly Counter<long> _hitTotal;
|
||||
private readonly Counter<long> _writeTotal;
|
||||
private readonly Histogram<double> _rebuildMs;
|
||||
|
||||
public LinksetCacheTelemetry(IMeterFactory meterFactory)
|
||||
{
|
||||
var meter = meterFactory.Create("StellaOps.Concelier.Linksets");
|
||||
_hitTotal = meter.CreateCounter<long>("lnm.cache.hit_total", unit: "hit", description: "Cache hits for LNM linksets");
|
||||
_writeTotal = meter.CreateCounter<long>("lnm.cache.write_total", unit: "write", description: "Cache writes for LNM linksets");
|
||||
_rebuildMs = meter.CreateHistogram<double>("lnm.cache.rebuild_ms", unit: "ms", description: "Synchronous rebuild latency for LNM cache");
|
||||
}
|
||||
|
||||
public void RecordHit(string? tenant, string source)
|
||||
{
|
||||
var tags = new TagList
|
||||
{
|
||||
{ "tenant", tenant ?? string.Empty },
|
||||
{ "source", source }
|
||||
};
|
||||
_hitTotal.Add(1, tags);
|
||||
}
|
||||
|
||||
public void RecordWrite(string? tenant, string source)
|
||||
{
|
||||
var tags = new TagList
|
||||
{
|
||||
{ "tenant", tenant ?? string.Empty },
|
||||
{ "source", source }
|
||||
};
|
||||
_writeTotal.Add(1, tags);
|
||||
}
|
||||
|
||||
public void RecordRebuild(string? tenant, string source, double elapsedMs)
|
||||
{
|
||||
var tags = new TagList
|
||||
{
|
||||
{ "tenant", tenant ?? string.Empty },
|
||||
{ "source", source }
|
||||
};
|
||||
_rebuildMs.Record(elapsedMs, tags);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ public static class ObservationPipelineServiceCollectionExtensions
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
|
||||
services.TryAddSingleton<IAdvisoryObservationSink, NullObservationSink>();
|
||||
services.TryAddSingleton<IAdvisoryObservationEventPublisher, NullObservationEventPublisher>();
|
||||
services.TryAddSingleton<IAdvisoryLinksetSink, NullLinksetSink>();
|
||||
services.TryAddSingleton<IAdvisoryLinksetLookup, NullLinksetLookup>();
|
||||
services.TryAddSingleton<IAdvisoryLinksetBackfillService, AdvisoryLinksetBackfillService>();
|
||||
@@ -25,6 +26,12 @@ public static class ObservationPipelineServiceCollectionExtensions
|
||||
=> Task.CompletedTask;
|
||||
}
|
||||
|
||||
private sealed class NullObservationEventPublisher : IAdvisoryObservationEventPublisher
|
||||
{
|
||||
public Task PublishAsync(AdvisoryObservationUpdatedEvent @event, CancellationToken cancellationToken)
|
||||
=> Task.CompletedTask;
|
||||
}
|
||||
|
||||
private sealed class NullLinksetSink : IAdvisoryLinksetSink
|
||||
{
|
||||
public Task UpsertAsync(AdvisoryLinkset linkset, CancellationToken cancellationToken)
|
||||
|
||||
@@ -32,7 +32,9 @@ public sealed class AdvisoryObservationAggregationTests
|
||||
null,
|
||||
new object?[] { ImmutableArray.Create(observation) })!;
|
||||
|
||||
Assert.Equal(ImmutableArray.Create("os:debian", "pkg:npm/foo"), aggregate.Scopes);
|
||||
Assert.Equal(2, aggregate.Scopes.Length);
|
||||
Assert.Contains("os:debian", aggregate.Scopes);
|
||||
Assert.Contains("pkg:npm/foo", aggregate.Scopes);
|
||||
Assert.Single(aggregate.Relationships);
|
||||
Assert.Equal("depends_on", aggregate.Relationships[0].Type);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user