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,372 @@
// ----------------------------------------------------------------------------
// Copyright (c) 2025 StellaOps contributors. All rights reserved.
// SPDX-License-Identifier: AGPL-3.0-or-later
// ----------------------------------------------------------------------------
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace StellaOps.Provcache.Oci;
/// <summary>
/// Builds OCI attestations for Provcache DecisionDigest objects.
/// The attestation follows the in-toto Statement format with a custom predicate type.
/// </summary>
public sealed class ProvcacheOciAttestationBuilder : IProvcacheOciAttestationBuilder
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
WriteIndented = false // Deterministic output
};
private readonly TimeProvider _timeProvider;
/// <summary>
/// Initializes a new instance of the <see cref="ProvcacheOciAttestationBuilder"/> class.
/// </summary>
public ProvcacheOciAttestationBuilder(TimeProvider? timeProvider = null)
{
_timeProvider = timeProvider ?? TimeProvider.System;
}
/// <inheritdoc/>
public ProvcacheOciAttestationResult Build(ProvcacheOciAttestationRequest request)
{
ArgumentNullException.ThrowIfNull(request);
ValidateRequest(request);
// Build subject from artifact reference
var subject = BuildSubject(request.ArtifactReference, request.ArtifactDigest);
// Build predicate from DecisionDigest
var predicate = BuildPredicate(request);
// Build the in-toto statement
var statement = new ProvcacheStatement
{
Subject = [subject],
Predicate = predicate
};
// Serialize to canonical JSON (deterministic)
var statementJson = JsonSerializer.Serialize(statement, SerializerOptions);
var statementBytes = Encoding.UTF8.GetBytes(statementJson);
// Build OCI annotations
var annotations = BuildAnnotations(request, predicate);
return new ProvcacheOciAttestationResult(
Statement: statement,
StatementJson: statementJson,
StatementBytes: statementBytes,
MediaType: ProvcachePredicateTypes.MediaType,
Annotations: annotations);
}
/// <inheritdoc/>
public ProvcacheOciAttachment CreateAttachment(ProvcacheOciAttestationRequest request)
{
var result = Build(request);
return new ProvcacheOciAttachment(
ArtifactReference: request.ArtifactReference,
MediaType: result.MediaType,
Payload: result.StatementJson,
PayloadBytes: result.StatementBytes,
Annotations: result.Annotations);
}
private static void ValidateRequest(ProvcacheOciAttestationRequest 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));
}
if (string.IsNullOrWhiteSpace(request.DecisionDigest.VeriKey))
{
throw new ArgumentException("DecisionDigest.VeriKey is required.", nameof(request));
}
}
private static ProvcacheSubject BuildSubject(string artifactReference, string artifactDigest)
{
// Parse digest format: "sha256:abc123..." or just the hash
var (algorithm, hash) = ParseDigest(artifactDigest);
// Extract name from reference (remove tag/digest suffix)
var name = ExtractArtifactName(artifactReference);
return new ProvcacheSubject
{
Name = name,
Digest = new Dictionary<string, string>(StringComparer.Ordinal)
{
[algorithm] = hash
}
};
}
private static (string algorithm, string hash) ParseDigest(string digest)
{
if (string.IsNullOrWhiteSpace(digest))
{
throw new ArgumentException("Digest cannot be empty.", nameof(digest));
}
// Handle "sha256:abc123..." format
var colonIndex = digest.IndexOf(':');
if (colonIndex > 0)
{
return (digest[..colonIndex], digest[(colonIndex + 1)..]);
}
// Assume SHA256 if no algorithm prefix
return ("sha256", digest);
}
private static string ExtractArtifactName(string reference)
{
// Remove @sha256:... digest suffix
var atIndex = reference.LastIndexOf('@');
if (atIndex > 0)
{
return reference[..atIndex];
}
// Remove :tag suffix, but be careful with port numbers
// e.g., ghcr.io:443/org/repo:tag -> ghcr.io:443/org/repo
var colonIndex = reference.LastIndexOf(':');
if (colonIndex > 0)
{
// Check if this is a port number (no slash after colon)
var slashIndex = reference.LastIndexOf('/');
if (slashIndex > colonIndex)
{
// The colon is before the last slash, so it's a port number
return reference;
}
return reference[..colonIndex];
}
return reference;
}
private ProvcachePredicate BuildPredicate(ProvcacheOciAttestationRequest request)
{
var digest = request.DecisionDigest;
var manifest = request.InputManifest;
return new ProvcachePredicate
{
VeriKey = digest.VeriKey,
VerdictHash = digest.VerdictHash,
ProofRoot = digest.ProofRoot,
TrustScore = digest.TrustScore,
TrustScoreBreakdown = BuildTrustBreakdown(digest.TrustScoreBreakdown),
InputManifest = BuildInputSummary(manifest, digest),
ReplaySeed = new ProvcacheReplaySeed
{
FeedIds = digest.ReplaySeed.FeedIds,
RuleIds = digest.ReplaySeed.RuleIds,
FrozenEpoch = digest.ReplaySeed.FrozenEpoch?.ToString("O")
},
CreatedAt = digest.CreatedAt.ToString("O"),
ExpiresAt = digest.ExpiresAt.ToString("O"),
VerdictSummary = request.VerdictSummary
};
}
private static ProvcacheTrustBreakdown? BuildTrustBreakdown(TrustScoreBreakdown? breakdown)
{
if (breakdown is null)
{
return null;
}
return new ProvcacheTrustBreakdown
{
Reachability = new ProvcacheTrustComponent
{
Score = breakdown.Reachability.Score,
Weight = breakdown.Reachability.Weight
},
SbomCompleteness = new ProvcacheTrustComponent
{
Score = breakdown.SbomCompleteness.Score,
Weight = breakdown.SbomCompleteness.Weight
},
VexCoverage = new ProvcacheTrustComponent
{
Score = breakdown.VexCoverage.Score,
Weight = breakdown.VexCoverage.Weight
},
PolicyFreshness = new ProvcacheTrustComponent
{
Score = breakdown.PolicyFreshness.Score,
Weight = breakdown.PolicyFreshness.Weight
},
SignerTrust = new ProvcacheTrustComponent
{
Score = breakdown.SignerTrust.Score,
Weight = breakdown.SignerTrust.Weight
}
};
}
private static ProvcacheInputSummary BuildInputSummary(InputManifest? manifest, DecisionDigest digest)
{
if (manifest is null)
{
// Fallback: extract from VeriKey components if available
return new ProvcacheInputSummary
{
SourceHash = ExtractComponentHash(digest.VeriKey, 0),
SbomHash = null,
VexSetHash = null,
PolicyHash = null,
SignerSetHash = null
};
}
return new ProvcacheInputSummary
{
SourceHash = manifest.SourceArtifact.Digest,
SbomHash = manifest.Sbom.Hash,
VexSetHash = manifest.Vex.SetHash,
PolicyHash = manifest.Policy.Hash,
SignerSetHash = manifest.Signers.SetHash
};
}
private static string ExtractComponentHash(string veriKey, int index)
{
// VeriKey format is typically sha256:<hash>
// This is a fallback; prefer using InputManifest
return veriKey;
}
private static IReadOnlyDictionary<string, string> BuildAnnotations(
ProvcacheOciAttestationRequest request,
ProvcachePredicate predicate)
{
var annotations = new Dictionary<string, string>(StringComparer.Ordinal)
{
// Standard OCI annotations
["org.opencontainers.image.title"] = "stellaops.provcache.decision",
["org.opencontainers.image.description"] = "Provcache decision attestation for provenance-cached vulnerability decisions",
["org.opencontainers.image.created"] = predicate.CreatedAt,
// StellaOps-specific annotations
["stellaops.provcache.verikey"] = predicate.VeriKey,
["stellaops.provcache.verdict-hash"] = predicate.VerdictHash,
["stellaops.provcache.proof-root"] = predicate.ProofRoot,
["stellaops.provcache.trust-score"] = predicate.TrustScore.ToString(),
["stellaops.provcache.expires-at"] = predicate.ExpiresAt
};
// Add optional tenant annotation
if (!string.IsNullOrWhiteSpace(request.TenantId))
{
annotations["stellaops.tenant"] = request.TenantId;
}
// Add optional scope annotation
if (!string.IsNullOrWhiteSpace(request.Scope))
{
annotations["stellaops.scope"] = request.Scope;
}
return annotations;
}
}
/// <summary>
/// Interface for building OCI attestations for Provcache decisions.
/// </summary>
public interface IProvcacheOciAttestationBuilder
{
/// <summary>
/// Builds an OCI attestation from a DecisionDigest.
/// </summary>
ProvcacheOciAttestationResult Build(ProvcacheOciAttestationRequest request);
/// <summary>
/// Creates an OCI attachment ready for pushing to a registry.
/// </summary>
ProvcacheOciAttachment CreateAttachment(ProvcacheOciAttestationRequest request);
}
/// <summary>
/// Request for building a Provcache OCI attestation.
/// </summary>
public sealed record ProvcacheOciAttestationRequest
{
/// <summary>
/// OCI artifact reference (e.g., ghcr.io/org/repo:tag or ghcr.io/org/repo@sha256:...).
/// </summary>
public required string ArtifactReference { get; init; }
/// <summary>
/// Digest of the artifact (e.g., sha256:abc123...).
/// </summary>
public required string ArtifactDigest { get; init; }
/// <summary>
/// The DecisionDigest to create an attestation for.
/// </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 for multi-tenant scenarios.
/// </summary>
public string? TenantId { get; init; }
/// <summary>
/// Optional: Scope identifier (e.g., environment, pipeline).
/// </summary>
public string? Scope { get; init; }
}
/// <summary>
/// Result of building an OCI attestation.
/// </summary>
public sealed record ProvcacheOciAttestationResult(
ProvcacheStatement Statement,
string StatementJson,
byte[] StatementBytes,
string MediaType,
IReadOnlyDictionary<string, string> Annotations);
/// <summary>
/// OCI attachment ready for pushing to a registry.
/// </summary>
public sealed record ProvcacheOciAttachment(
string ArtifactReference,
string MediaType,
string Payload,
byte[] PayloadBytes,
IReadOnlyDictionary<string, string> Annotations);