Add new features and tests for AirGap and Time modules
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:
master
2025-11-20 23:29:54 +02:00
parent 65b1599229
commit 79b8e53441
182 changed files with 6660 additions and 1242 deletions

View File

@@ -0,0 +1,204 @@
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace StellaOps.Excititor.Connectors.Abstractions.Trust;
public sealed record ConnectorSignerMetadataSet(
string SchemaVersion,
DateTimeOffset GeneratedAt,
ImmutableArray<ConnectorSignerMetadata> Connectors)
{
private readonly ImmutableDictionary<string, ConnectorSignerMetadata> _byId =
Connectors.ToImmutableDictionary(x => x.ConnectorId, StringComparer.OrdinalIgnoreCase);
public bool TryGet(string connectorId, [NotNullWhen(true)] out ConnectorSignerMetadata? metadata)
=> _byId.TryGetValue(connectorId, out metadata);
}
public sealed record ConnectorSignerMetadata(
string ConnectorId,
string ProviderName,
string ProviderSlug,
string IssuerTier,
ImmutableArray<ConnectorSignerSigner> Signers,
ConnectorSignerBundleRef? Bundle,
string? ValidFrom,
string? ValidTo,
bool Revoked,
string? Notes);
public sealed record ConnectorSignerSigner(
string Usage,
ImmutableArray<ConnectorSignerFingerprint> Fingerprints,
string? KeyLocator,
ImmutableArray<string> CertificateChain);
public sealed record ConnectorSignerFingerprint(
string Alg,
string Format,
string Value);
public sealed record ConnectorSignerBundleRef(
string Kind,
string Uri,
string? Digest,
DateTimeOffset? PublishedAt);
public static class ConnectorSignerMetadataLoader
{
public static ConnectorSignerMetadataSet? TryLoad(string? path, Stream? overrideStream = null)
{
if (string.IsNullOrWhiteSpace(path) && overrideStream is null)
{
return null;
}
try
{
using var stream = overrideStream ?? File.OpenRead(path!);
var root = JsonNode.Parse(stream, new JsonNodeOptions { PropertyNameCaseInsensitive = true });
if (root is null)
{
return null;
}
var version = root["schemaVersion"]?.GetValue<string?>() ?? "0.0.0";
var generatedAt = root["generatedAt"]?.GetValue<DateTimeOffset?>() ?? DateTimeOffset.MinValue;
var connectorsNode = root["connectors"] as JsonArray;
if (connectorsNode is null || connectorsNode.Count == 0)
{
return null;
}
var connectors = connectorsNode
.Select(ParseConnector)
.Where(c => c is not null)
.Select(c => c!)
.OrderBy(c => c.ConnectorId, StringComparer.Ordinal)
.ToImmutableArray();
return new ConnectorSignerMetadataSet(version, generatedAt, connectors);
}
catch
{
return null;
}
}
private static ConnectorSignerMetadata? ParseConnector(JsonNode? node)
{
if (node is not JsonObject obj)
{
return null;
}
var id = obj["connectorId"]?.GetValue<string?>();
var providerName = obj["provider"]?["name"]?.GetValue<string?>();
var providerSlug = obj["provider"]?["slug"]?.GetValue<string?>();
var issuerTier = obj["issuerTier"]?.GetValue<string?>();
var signers = (obj["signers"] as JsonArray)?.Select(ParseSigner)
.Where(x => x is not null)
.Select(x => x!)
.ToImmutableArray() ?? ImmutableArray<ConnectorSignerSigner>.Empty;
if (string.IsNullOrWhiteSpace(id) || string.IsNullOrWhiteSpace(providerName) || signers.Length == 0)
{
return null;
}
return new ConnectorSignerMetadata(
id!,
providerName!,
providerSlug ?? providerName!,
issuerTier ?? "untrusted",
signers,
ParseBundle(obj["bundle"]),
obj["validFrom"]?.GetValue<string?>(),
obj["validTo"]?.GetValue<string?>(),
obj["revoked"]?.GetValue<bool?>() ?? false,
obj["notes"]?.GetValue<string?>());
}
private static ConnectorSignerSigner? ParseSigner(JsonNode? node)
{
if (node is not JsonObject obj)
{
return null;
}
var usage = obj["usage"]?.GetValue<string?>();
var fps = (obj["fingerprints"] as JsonArray)?.Select(ParseFingerprint)
.Where(x => x is not null)
.Select(x => x!)
.ToImmutableArray() ?? ImmutableArray<ConnectorSignerFingerprint>.Empty;
if (string.IsNullOrWhiteSpace(usage) || fps.IsDefaultOrEmpty)
{
return null;
}
var chain = (obj["certificateChain"] as JsonArray)?.Select(x => x?.GetValue<string?>())
.Where(v => !string.IsNullOrWhiteSpace(v))
.Select(v => v!)
.ToImmutableArray() ?? ImmutableArray<string>.Empty;
return new ConnectorSignerSigner(
usage!,
fps,
obj["keyLocator"]?.GetValue<string?>(),
chain);
}
private static ConnectorSignerFingerprint? ParseFingerprint(JsonNode? node)
{
if (node is not JsonObject obj)
{
return null;
}
var alg = obj["alg"]?.GetValue<string?>();
var format = obj["format"]?.GetValue<string?>();
var value = obj["value"]?.GetValue<string?>();
if (string.IsNullOrWhiteSpace(alg) || string.IsNullOrWhiteSpace(format) || string.IsNullOrWhiteSpace(value))
{
return null;
}
return new ConnectorSignerFingerprint(alg!, format!, value!);
}
private static ConnectorSignerBundleRef? ParseBundle(JsonNode? node)
{
if (node is not JsonObject obj)
{
return null;
}
var kind = obj["kind"]?.GetValue<string?>();
var uri = obj["uri"]?.GetValue<string?>();
if (string.IsNullOrWhiteSpace(kind) || string.IsNullOrWhiteSpace(uri))
{
return null;
}
DateTimeOffset? published = null;
if (obj["publishedAt"] is JsonNode publishedNode && publishedNode.GetValue<string?>() is { } publishedString)
{
if (DateTimeOffset.TryParse(publishedString, out var parsed))
{
published = parsed;
}
}
return new ConnectorSignerBundleRef(
kind!,
uri!,
obj["digest"]?.GetValue<string?>(),
published);
}
}

View File

@@ -0,0 +1,80 @@
using System;
using System.IO;
using System.Linq;
using Microsoft.Extensions.Logging;
namespace StellaOps.Excititor.Connectors.Abstractions.Trust;
public static class ConnectorSignerMetadataEnricher
{
private static readonly object Sync = new();
private static ConnectorSignerMetadataSet? _cached;
private static string? _cachedPath;
private const string EnvVar = "STELLAOPS_CONNECTOR_SIGNER_METADATA_PATH";
public static void Enrich(
VexConnectorMetadataBuilder builder,
string connectorId,
ILogger? logger = null,
string? metadataPath = null)
{
ArgumentNullException.ThrowIfNull(builder);
if (string.IsNullOrWhiteSpace(connectorId)) return;
var path = metadataPath ?? Environment.GetEnvironmentVariable(EnvVar);
if (string.IsNullOrWhiteSpace(path))
{
return;
}
var metadata = LoadCached(path, logger);
if (metadata is null || !metadata.TryGet(connectorId, out var connector))
{
return;
}
builder
.Add("vex.provenance.trust.issuerTier", connector.IssuerTier)
.Add("vex.provenance.trust.signers", string.Join(';', connector.Signers.SelectMany(s => s.Fingerprints.Select(fp => fp.Value))))
.Add("vex.provenance.trust.provider", connector.ProviderSlug);
if (connector.Bundle is { } bundle)
{
builder
.Add("vex.provenance.bundle.kind", bundle.Kind)
.Add("vex.provenance.bundle.uri", bundle.Uri)
.Add("vex.provenance.bundle.digest", bundle.Digest)
.Add("vex.provenance.bundle.publishedAt", bundle.PublishedAt?.ToUniversalTime().ToString("O"));
}
}
private static ConnectorSignerMetadataSet? LoadCached(string path, ILogger? logger)
{
if (!string.Equals(path, _cachedPath, StringComparison.OrdinalIgnoreCase) || _cached is null)
{
lock (Sync)
{
if (!string.Equals(path, _cachedPath, StringComparison.OrdinalIgnoreCase) || _cached is null)
{
if (!File.Exists(path))
{
logger?.LogDebug("Connector signer metadata file not found at {Path}; skipping enrichment.", path);
_cached = null;
}
else
{
_cached = ConnectorSignerMetadataLoader.TryLoad(path);
_cachedPath = path;
if (_cached is null)
{
logger?.LogWarning("Failed to load connector signer metadata from {Path}.", path);
}
}
}
}
}
return _cached;
}
}

View File

@@ -12,6 +12,7 @@ using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Excititor.Connectors.Abstractions;
using StellaOps.Excititor.Connectors.Abstractions.Trust;
using StellaOps.Excititor.Connectors.MSRC.CSAF.Authentication;
using StellaOps.Excititor.Connectors.MSRC.CSAF.Configuration;
using StellaOps.Excititor.Core;
@@ -276,6 +277,8 @@ public sealed class MsrcCsafConnector : VexConnectorBase
{
builder.Add("http.lastModified", lastModified.ToString("O"));
}
ConnectorSignerMetadataEnricher.Enrich(builder, Descriptor.Id, _logger);
});
return CreateRawDocument(VexDocumentFormat.Csaf, documentUri, payload, metadata);

View File

@@ -1,11 +1,13 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Runtime.CompilerServices;
using Microsoft.Extensions.Logging;
using StellaOps.Excititor.Connectors.Abstractions;
using StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.Configuration;
using StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.Discovery;
using StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.Fetch;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Runtime.CompilerServices;
using System.Linq;
using Microsoft.Extensions.Logging;
using StellaOps.Excititor.Connectors.Abstractions;
using StellaOps.Excititor.Connectors.Abstractions.Trust;
using StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.Configuration;
using StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.Discovery;
using StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.Fetch;
using StellaOps.Excititor.Core;
namespace StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest;
@@ -187,11 +189,11 @@ public sealed class OciOpenVexAttestationConnector : VexConnectorBase
}
}
if (signature is not null)
{
builder["vex.signature.type"] = signature.Type;
if (!string.IsNullOrWhiteSpace(signature.Subject))
{
if (signature is not null)
{
builder["vex.signature.type"] = signature.Type;
if (!string.IsNullOrWhiteSpace(signature.Subject))
{
builder["vex.signature.subject"] = signature.Subject!;
}
@@ -211,11 +213,19 @@ public sealed class OciOpenVexAttestationConnector : VexConnectorBase
}
if (!string.IsNullOrWhiteSpace(signature.TransparencyLogReference))
{
builder["vex.signature.transparencyLogReference"] = signature.TransparencyLogReference!;
}
}
return builder.ToImmutable();
}
}
{
builder["vex.signature.transparencyLogReference"] = signature.TransparencyLogReference!;
}
}
var metadataBuilder = new VexConnectorMetadataBuilder();
metadataBuilder.AddRange(builder.Select(kv => new KeyValuePair<string, string?>(kv.Key, kv.Value)));
ConnectorSignerMetadataEnricher.Enrich(metadataBuilder, Descriptor.Id, Logger);
foreach (var kv in metadataBuilder.Build())
{
builder[kv.Key] = kv.Value;
}
return builder.ToImmutable();
}
}

View File

@@ -8,10 +8,11 @@ using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using StellaOps.Excititor.Connectors.Abstractions;
using StellaOps.Excititor.Connectors.Oracle.CSAF.Configuration;
using StellaOps.Excititor.Connectors.Oracle.CSAF.Metadata;
using Microsoft.Extensions.Logging;
using StellaOps.Excititor.Connectors.Abstractions;
using StellaOps.Excititor.Connectors.Abstractions.Trust;
using StellaOps.Excititor.Connectors.Oracle.CSAF.Configuration;
using StellaOps.Excititor.Connectors.Oracle.CSAF.Metadata;
using StellaOps.Excititor.Core;
using StellaOps.Excititor.Storage.Mongo;
@@ -260,11 +261,13 @@ public sealed class OracleCsafConnector : VexConnectorBase
builder.Add("oracle.csaf.sha256", NormalizeDigest(entry.Sha256));
builder.Add("oracle.csaf.size", entry.Size?.ToString(CultureInfo.InvariantCulture));
if (!entry.Products.IsDefaultOrEmpty)
{
builder.Add("oracle.csaf.products", string.Join(",", entry.Products));
}
});
if (!entry.Products.IsDefaultOrEmpty)
{
builder.Add("oracle.csaf.products", string.Join(",", entry.Products));
}
ConnectorSignerMetadataEnricher.Enrich(builder, Descriptor.Id, _logger);
});
return CreateRawDocument(VexDocumentFormat.Csaf, entry.DocumentUri, payload.AsMemory(), metadata);
}

View File

@@ -10,6 +10,7 @@ using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using StellaOps.Excititor.Connectors.Abstractions;
using StellaOps.Excititor.Connectors.Abstractions.Trust;
using StellaOps.Excititor.Connectors.Ubuntu.CSAF.Configuration;
using StellaOps.Excititor.Connectors.Ubuntu.CSAF.Metadata;
using StellaOps.Excititor.Core;
@@ -459,6 +460,8 @@ public sealed class UbuntuCsafConnector : VexConnectorBase
builder
.Add("vex.provenance.trust.tier", tier)
.Add("vex.provenance.trust.note", $"tier={tier};weight={provider.Trust.Weight.ToString("0.###", CultureInfo.InvariantCulture)}");
ConnectorSignerMetadataEnricher.Enrich(builder, Descriptor.Id, _logger);
}
private static async ValueTask UpsertProviderAsync(IServiceProvider services, VexProvider provider, CancellationToken cancellationToken)