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,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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user