save development progress

This commit is contained in:
StellaOps Bot
2025-12-25 23:09:58 +02:00
parent d71853ad7e
commit aa70af062e
351 changed files with 37683 additions and 150156 deletions

View File

@@ -0,0 +1,291 @@
// ----------------------------------------------------------------------------
// Copyright (c) 2025 StellaOps contributors. All rights reserved.
// SPDX-License-Identifier: AGPL-3.0-or-later
// ----------------------------------------------------------------------------
using StellaOps.Provcache;
using StellaOps.Provcache.Oci;
namespace StellaOps.ExportCenter.Core.Provcache;
/// <summary>
/// Integrates Provcache OCI attestations with the ExportCenter OCI push workflow.
/// Enables automatic attachment of DecisionDigest attestations to container images.
/// </summary>
public sealed class ProvcacheOciExporter : IProvcacheOciExporter
{
private readonly IProvcacheOciAttestationBuilder _attestationBuilder;
private readonly TimeProvider _timeProvider;
/// <summary>
/// Initializes a new instance of the <see cref="ProvcacheOciExporter"/> class.
/// </summary>
public ProvcacheOciExporter(
IProvcacheOciAttestationBuilder attestationBuilder,
TimeProvider? timeProvider = null)
{
_attestationBuilder = attestationBuilder ?? throw new ArgumentNullException(nameof(attestationBuilder));
_timeProvider = timeProvider ?? TimeProvider.System;
}
/// <inheritdoc/>
public ProvcacheExportLayerContent CreateAttestationLayer(ProvcacheExportRequest request)
{
ArgumentNullException.ThrowIfNull(request);
ValidateRequest(request);
// Build the attestation
var attestationRequest = new ProvcacheOciAttestationRequest
{
ArtifactReference = request.ArtifactReference,
ArtifactDigest = request.ArtifactDigest,
DecisionDigest = request.DecisionDigest,
InputManifest = request.InputManifest,
VerdictSummary = request.VerdictSummary,
TenantId = request.TenantId,
Scope = request.Scope
};
var attachment = _attestationBuilder.CreateAttachment(attestationRequest);
return new ProvcacheExportLayerContent(
MediaType: ProvcachePredicateTypes.MediaType,
Content: attachment.PayloadBytes,
ContentJson: attachment.Payload,
Digest: ComputeDigest(attachment.PayloadBytes),
Annotations: attachment.Annotations);
}
/// <inheritdoc/>
public ProvcacheExportResult BuildExport(ProvcacheExportRequest request)
{
ArgumentNullException.ThrowIfNull(request);
var layer = CreateAttestationLayer(request);
var manifest = BuildManifest(request, layer);
return new ProvcacheExportResult(
Layer: layer,
Manifest: manifest,
ArtifactReference: request.ArtifactReference,
SubjectDigest: request.ArtifactDigest,
CreatedAt: _timeProvider.GetUtcNow());
}
/// <inheritdoc/>
public bool ShouldAttachAttestation(ProvcacheAttachmentPolicy policy, DecisionDigest digest)
{
ArgumentNullException.ThrowIfNull(policy);
ArgumentNullException.ThrowIfNull(digest);
// Check if attestations are enabled
if (!policy.Enabled)
{
return false;
}
// Check trust score threshold
if (digest.TrustScore < policy.MinimumTrustScore)
{
return false;
}
// Check if the digest is expired
if (digest.ExpiresAt <= _timeProvider.GetUtcNow())
{
return policy.AttachExpired;
}
return true;
}
private static void ValidateRequest(ProvcacheExportRequest request)
{
if (string.IsNullOrWhiteSpace(request.ArtifactReference))
{
throw new ArgumentException("Artifact reference is required.", nameof(request));
}
if (string.IsNullOrWhiteSpace(request.ArtifactDigest))
{
throw new ArgumentException("Artifact digest is required.", nameof(request));
}
if (request.DecisionDigest is null)
{
throw new ArgumentException("DecisionDigest is required.", nameof(request));
}
}
private static string ComputeDigest(byte[] content)
{
var hash = System.Security.Cryptography.SHA256.HashData(content);
return $"sha256:{Convert.ToHexStringLower(hash)}";
}
private ProvcacheExportManifest BuildManifest(
ProvcacheExportRequest request,
ProvcacheExportLayerContent layer)
{
var annotations = new Dictionary<string, string>(StringComparer.Ordinal)
{
["org.opencontainers.image.created"] = _timeProvider.GetUtcNow().ToString("O"),
["org.opencontainers.image.title"] = "stellaops.provcache.decision",
["org.opencontainers.image.description"] = "Provcache decision attestation",
["stellaops.provcache.verikey"] = request.DecisionDigest.VeriKey,
["stellaops.provcache.trust-score"] = request.DecisionDigest.TrustScore.ToString()
};
if (!string.IsNullOrWhiteSpace(request.TenantId))
{
annotations["stellaops.tenant"] = request.TenantId;
}
return new ProvcacheExportManifest(
SchemaVersion: 2,
MediaType: "application/vnd.oci.image.manifest.v1+json",
ArtifactType: ProvcachePredicateTypes.MediaType,
SubjectDigest: request.ArtifactDigest,
LayerDigest: layer.Digest,
LayerMediaType: layer.MediaType,
LayerSize: layer.Content.Length,
Annotations: annotations);
}
}
/// <summary>
/// Interface for exporting Provcache OCI attestations.
/// </summary>
public interface IProvcacheOciExporter
{
/// <summary>
/// Creates an attestation layer content for pushing to OCI registry.
/// </summary>
ProvcacheExportLayerContent CreateAttestationLayer(ProvcacheExportRequest request);
/// <summary>
/// Builds a complete export result with manifest and layer.
/// </summary>
ProvcacheExportResult BuildExport(ProvcacheExportRequest request);
/// <summary>
/// Determines if an attestation should be attached based on policy.
/// </summary>
bool ShouldAttachAttestation(ProvcacheAttachmentPolicy policy, DecisionDigest digest);
}
/// <summary>
/// Request for exporting a Provcache attestation.
/// </summary>
public sealed record ProvcacheExportRequest
{
/// <summary>
/// OCI artifact reference to attach the attestation to.
/// </summary>
public required string ArtifactReference { get; init; }
/// <summary>
/// Digest of the artifact.
/// </summary>
public required string ArtifactDigest { get; init; }
/// <summary>
/// The DecisionDigest to export.
/// </summary>
public required DecisionDigest DecisionDigest { get; init; }
/// <summary>
/// Optional: Full InputManifest for detailed provenance.
/// </summary>
public InputManifest? InputManifest { get; init; }
/// <summary>
/// Optional: Summary of verdicts.
/// </summary>
public ProvcacheVerdictSummary? VerdictSummary { get; init; }
/// <summary>
/// Optional: Tenant identifier.
/// </summary>
public string? TenantId { get; init; }
/// <summary>
/// Optional: Scope identifier.
/// </summary>
public string? Scope { get; init; }
}
/// <summary>
/// Layer content for OCI push.
/// </summary>
public sealed record ProvcacheExportLayerContent(
string MediaType,
byte[] Content,
string ContentJson,
string Digest,
IReadOnlyDictionary<string, string> Annotations);
/// <summary>
/// Complete export result for OCI push.
/// </summary>
public sealed record ProvcacheExportResult(
ProvcacheExportLayerContent Layer,
ProvcacheExportManifest Manifest,
string ArtifactReference,
string SubjectDigest,
DateTimeOffset CreatedAt);
/// <summary>
/// OCI manifest for the attestation artifact.
/// </summary>
public sealed record ProvcacheExportManifest(
int SchemaVersion,
string MediaType,
string ArtifactType,
string SubjectDigest,
string LayerDigest,
string LayerMediaType,
long LayerSize,
IReadOnlyDictionary<string, string> Annotations);
/// <summary>
/// Policy for automatic attestation attachment.
/// </summary>
public sealed record ProvcacheAttachmentPolicy
{
/// <summary>
/// Whether automatic attestation is enabled.
/// </summary>
public bool Enabled { get; init; } = true;
/// <summary>
/// Minimum trust score required for attestation attachment.
/// </summary>
public int MinimumTrustScore { get; init; } = 0;
/// <summary>
/// Whether to attach attestations for expired decisions.
/// </summary>
public bool AttachExpired { get; init; } = false;
/// <summary>
/// Registry patterns to include (glob patterns).
/// Empty means all registries.
/// </summary>
public IReadOnlyList<string> IncludeRegistries { get; init; } = [];
/// <summary>
/// Registry patterns to exclude (glob patterns).
/// </summary>
public IReadOnlyList<string> ExcludeRegistries { get; init; } = [];
/// <summary>
/// Creates a default policy with attestations enabled.
/// </summary>
public static ProvcacheAttachmentPolicy Default => new();
/// <summary>
/// Creates a disabled policy.
/// </summary>
public static ProvcacheAttachmentPolicy Disabled => new() { Enabled = false };
}

View File

@@ -0,0 +1,183 @@
// ----------------------------------------------------------------------------
// Copyright (c) 2025 StellaOps contributors. All rights reserved.
// SPDX-License-Identifier: AGPL-3.0-or-later
// ----------------------------------------------------------------------------
namespace StellaOps.ExportCenter.Core.Provcache;
/// <summary>
/// Configuration options for Provcache OCI attestation features.
/// </summary>
public sealed class ProvcacheOciOptions
{
/// <summary>
/// Configuration section name.
/// </summary>
public const string SectionName = "Provcache:Oci";
/// <summary>
/// Whether OCI attestation features are enabled.
/// Default: true.
/// </summary>
public bool Enabled { get; set; } = true;
/// <summary>
/// Whether to automatically attach attestations to pushed images.
/// Default: true.
/// </summary>
public bool AutoAttach { get; set; } = true;
/// <summary>
/// Minimum trust score required for automatic attestation attachment.
/// Default: 0 (attach all decisions).
/// </summary>
public int MinimumTrustScore { get; set; } = 0;
/// <summary>
/// Whether to attach attestations for expired decisions.
/// Default: false.
/// </summary>
public bool AttachExpiredDecisions { get; set; } = false;
/// <summary>
/// Whether to sign attestations with DSSE.
/// Default: true.
/// </summary>
public bool SignAttestations { get; set; } = true;
/// <summary>
/// Key identifier for signing attestations.
/// If null, uses the default signing key.
/// </summary>
public string? SigningKeyId { get; set; }
/// <summary>
/// Whether to include the full InputManifest in attestations.
/// Default: true.
/// </summary>
public bool IncludeInputManifest { get; set; } = true;
/// <summary>
/// Whether to include verdict summary in attestations.
/// Default: true.
/// </summary>
public bool IncludeVerdictSummary { get; set; } = true;
/// <summary>
/// Registry patterns to include for auto-attach (glob patterns).
/// Empty means all registries.
/// Example: ["ghcr.io/*", "docker.io/stellaops/*"]
/// </summary>
public string[] IncludeRegistries { get; set; } = [];
/// <summary>
/// Registry patterns to exclude from auto-attach (glob patterns).
/// Example: ["registry.internal/*"]
/// </summary>
public string[] ExcludeRegistries { get; set; } = [];
/// <summary>
/// Timeout for OCI push operations in seconds.
/// Default: 60.
/// </summary>
public int PushTimeoutSeconds { get; set; } = 60;
/// <summary>
/// Number of retries for failed push operations.
/// Default: 3.
/// </summary>
public int PushRetryCount { get; set; } = 3;
/// <summary>
/// Delay between retries in milliseconds.
/// Default: 1000.
/// </summary>
public int PushRetryDelayMs { get; set; } = 1000;
/// <summary>
/// Whether to fail the overall operation if attestation push fails.
/// Default: false (attestation push failures are logged but don't block).
/// </summary>
public bool FailOnAttestationError { get; set; } = false;
/// <summary>
/// Additional annotations to add to all attestations.
/// </summary>
public Dictionary<string, string> CustomAnnotations { get; set; } = [];
/// <summary>
/// Converts this options instance to an attachment policy.
/// </summary>
public ProvcacheAttachmentPolicy ToAttachmentPolicy() => new()
{
Enabled = Enabled && AutoAttach,
MinimumTrustScore = MinimumTrustScore,
AttachExpired = AttachExpiredDecisions,
IncludeRegistries = IncludeRegistries,
ExcludeRegistries = ExcludeRegistries
};
}
/// <summary>
/// Validates <see cref="ProvcacheOciOptions"/>.
/// </summary>
public static class ProvcacheOciOptionsValidator
{
/// <summary>
/// Validates the options and returns any validation errors.
/// </summary>
public static IEnumerable<string> Validate(ProvcacheOciOptions options)
{
ArgumentNullException.ThrowIfNull(options);
if (options.MinimumTrustScore is < 0 or > 100)
{
yield return "MinimumTrustScore must be between 0 and 100.";
}
if (options.PushTimeoutSeconds <= 0)
{
yield return "PushTimeoutSeconds must be greater than 0.";
}
if (options.PushRetryCount < 0)
{
yield return "PushRetryCount must be non-negative.";
}
if (options.PushRetryDelayMs < 0)
{
yield return "PushRetryDelayMs must be non-negative.";
}
// Validate registry patterns are valid globs
foreach (var pattern in options.IncludeRegistries)
{
if (string.IsNullOrWhiteSpace(pattern))
{
yield return "IncludeRegistries contains an empty pattern.";
}
}
foreach (var pattern in options.ExcludeRegistries)
{
if (string.IsNullOrWhiteSpace(pattern))
{
yield return "ExcludeRegistries contains an empty pattern.";
}
}
}
/// <summary>
/// Throws if the options are invalid.
/// </summary>
public static void ValidateAndThrow(ProvcacheOciOptions options)
{
var errors = Validate(options).ToList();
if (errors.Count > 0)
{
throw new InvalidOperationException(
$"Invalid ProvcacheOciOptions: {string.Join("; ", errors)}");
}
}
}