115 lines
3.4 KiB
C#
115 lines
3.4 KiB
C#
// <copyright file="FacetSeal.cs" company="StellaOps">
|
|
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
|
|
// </copyright>
|
|
|
|
using System.Collections.Immutable;
|
|
|
|
namespace StellaOps.Facet;
|
|
|
|
/// <summary>
|
|
/// Sealed manifest of facets for an image at a point in time.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// A FacetSeal captures the cryptographic state of all facets in an image,
|
|
/// enabling drift detection and quota enforcement on subsequent scans.
|
|
/// </para>
|
|
/// <para>
|
|
/// The seal can be optionally signed with DSSE for authenticity verification.
|
|
/// </para>
|
|
/// </remarks>
|
|
public sealed record FacetSeal
|
|
{
|
|
/// <summary>
|
|
/// Current schema version.
|
|
/// </summary>
|
|
public const string CurrentSchemaVersion = "1.0.0";
|
|
|
|
/// <summary>
|
|
/// Gets the schema version for forward compatibility.
|
|
/// </summary>
|
|
public string SchemaVersion { get; init; } = CurrentSchemaVersion;
|
|
|
|
/// <summary>
|
|
/// Gets the image digest this seal applies to.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Format: "sha256:{hex}" or "sha512:{hex}".
|
|
/// </remarks>
|
|
public required string ImageDigest { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gets when the seal was created.
|
|
/// </summary>
|
|
public required DateTimeOffset CreatedAt { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gets the optional build attestation reference (in-toto provenance).
|
|
/// </summary>
|
|
public string? BuildAttestationRef { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gets the individual facet seals.
|
|
/// </summary>
|
|
public required ImmutableArray<FacetEntry> Facets { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gets the quota configuration per facet.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Keys are facet IDs. Facets without explicit quotas use default values.
|
|
/// </remarks>
|
|
public ImmutableDictionary<string, FacetQuota>? Quotas { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gets the combined Merkle root of all facet roots.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Computed from facet Merkle roots in sorted order by FacetId.
|
|
/// Enables single-value integrity verification.
|
|
/// </remarks>
|
|
public required string CombinedMerkleRoot { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gets the optional DSSE signature over canonical form.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Base64-encoded DSSE envelope when the seal is signed.
|
|
/// </remarks>
|
|
public string? Signature { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gets the signing key identifier, if signed.
|
|
/// </summary>
|
|
public string? SigningKeyId { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gets whether this seal is signed.
|
|
/// </summary>
|
|
public bool IsSigned => !string.IsNullOrEmpty(Signature);
|
|
|
|
/// <summary>
|
|
/// Gets the quota for a specific facet, or default if not configured.
|
|
/// </summary>
|
|
/// <param name="facetId">The facet identifier.</param>
|
|
/// <returns>The configured quota or <see cref="FacetQuota.Default"/>.</returns>
|
|
public FacetQuota GetQuota(string facetId)
|
|
{
|
|
if (Quotas is not null &&
|
|
Quotas.TryGetValue(facetId, out var quota))
|
|
{
|
|
return quota;
|
|
}
|
|
|
|
return FacetQuota.Default;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a facet entry by ID.
|
|
/// </summary>
|
|
/// <param name="facetId">The facet identifier.</param>
|
|
/// <returns>The facet entry or null if not found.</returns>
|
|
public FacetEntry? GetFacet(string facetId)
|
|
=> Facets.FirstOrDefault(f => f.FacetId == facetId);
|
|
}
|