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

@@ -17,6 +17,7 @@ public sealed class DecisionDigestBuilder
private DateTimeOffset? _createdAt;
private DateTimeOffset? _expiresAt;
private int? _trustScore;
private TrustScoreBreakdown? _trustScoreBreakdown;
private readonly ProvcacheOptions _options;
private readonly TimeProvider _timeProvider;
@@ -217,7 +218,20 @@ public sealed class DecisionDigestBuilder
}
/// <summary>
/// Computes trust score from component scores using weighted formula.
/// Sets the trust score from a breakdown, computing the total automatically.
/// </summary>
/// <param name="breakdown">The trust score breakdown with component scores.</param>
public DecisionDigestBuilder WithTrustScoreBreakdown(TrustScoreBreakdown breakdown)
{
ArgumentNullException.ThrowIfNull(breakdown);
_trustScoreBreakdown = breakdown;
_trustScore = breakdown.ComputeTotal();
return this;
}
/// <summary>
/// Computes trust score from component scores using weighted formula,
/// and stores the breakdown for API responses.
/// </summary>
/// <param name="reachabilityScore">Reachability analysis coverage (0-100).</param>
/// <param name="sbomCompletenessScore">SBOM completeness (0-100).</param>
@@ -231,14 +245,16 @@ public sealed class DecisionDigestBuilder
int policyFreshnessScore,
int signerTrustScore)
{
// Weights from documentation:
// Reachability: 25%, SBOM: 20%, VEX: 20%, Policy: 15%, Signer: 20%
_trustScore = (int)Math.Round(
reachabilityScore * 0.25 +
sbomCompletenessScore * 0.20 +
vexCoverageScore * 0.20 +
policyFreshnessScore * 0.15 +
signerTrustScore * 0.20);
// Create breakdown with standard weights
_trustScoreBreakdown = TrustScoreBreakdown.CreateDefault(
reachabilityScore,
sbomCompletenessScore,
vexCoverageScore,
policyFreshnessScore,
signerTrustScore);
// Compute total from breakdown
_trustScore = _trustScoreBreakdown.ComputeTotal();
// Clamp to valid range
_trustScore = Math.Clamp(_trustScore.Value, 0, 100);
@@ -263,7 +279,8 @@ public sealed class DecisionDigestBuilder
ReplaySeed = _replaySeed!,
CreatedAt = _createdAt!.Value,
ExpiresAt = _expiresAt!.Value,
TrustScore = _trustScore!.Value
TrustScore = _trustScore!.Value,
TrustScoreBreakdown = _trustScoreBreakdown
};
}
@@ -279,6 +296,7 @@ public sealed class DecisionDigestBuilder
_createdAt = null;
_expiresAt = null;
_trustScore = null;
_trustScoreBreakdown = null;
return this;
}

View File

@@ -52,6 +52,99 @@ public sealed record DecisionDigest
/// Based on reachability, SBOM completeness, VEX coverage, policy freshness, and signer trust.
/// </summary>
public required int TrustScore { get; init; }
/// <summary>
/// Breakdown of trust score by component.
/// Each component has its own score (0-100) and weight that contributed to <see cref="TrustScore"/>.
/// </summary>
public TrustScoreBreakdown? TrustScoreBreakdown { get; init; }
}
/// <summary>
/// Breakdown of trust score by component, showing contribution from each evidence type.
/// </summary>
public sealed record TrustScoreBreakdown
{
/// <summary>
/// Reachability evidence contribution (weight: 25%).
/// Based on call graph / static analysis evidence.
/// </summary>
public required TrustScoreComponent Reachability { get; init; }
/// <summary>
/// SBOM completeness contribution (weight: 20%).
/// Based on package coverage and license data.
/// </summary>
public required TrustScoreComponent SbomCompleteness { get; init; }
/// <summary>
/// VEX statement coverage contribution (weight: 20%).
/// Based on vendor statements and OpenVEX coverage.
/// </summary>
public required TrustScoreComponent VexCoverage { get; init; }
/// <summary>
/// Policy freshness contribution (weight: 15%).
/// Based on last policy update timestamp.
/// </summary>
public required TrustScoreComponent PolicyFreshness { get; init; }
/// <summary>
/// Signer trust contribution (weight: 20%).
/// Based on signer reputation and key age.
/// </summary>
public required TrustScoreComponent SignerTrust { get; init; }
/// <summary>
/// Computes weighted total score from all components.
/// </summary>
public int ComputeTotal()
{
return (int)Math.Round(
Reachability.Score * Reachability.Weight +
SbomCompleteness.Score * SbomCompleteness.Weight +
VexCoverage.Score * VexCoverage.Weight +
PolicyFreshness.Score * PolicyFreshness.Weight +
SignerTrust.Score * SignerTrust.Weight);
}
/// <summary>
/// Creates a default breakdown with standard weights.
/// </summary>
public static TrustScoreBreakdown CreateDefault(
int reachabilityScore = 0,
int sbomScore = 0,
int vexScore = 0,
int policyScore = 0,
int signerScore = 0) => new()
{
Reachability = new TrustScoreComponent { Score = reachabilityScore, Weight = 0.25m },
SbomCompleteness = new TrustScoreComponent { Score = sbomScore, Weight = 0.20m },
VexCoverage = new TrustScoreComponent { Score = vexScore, Weight = 0.20m },
PolicyFreshness = new TrustScoreComponent { Score = policyScore, Weight = 0.15m },
SignerTrust = new TrustScoreComponent { Score = signerScore, Weight = 0.20m }
};
}
/// <summary>
/// Individual component of trust score with its score and weight.
/// </summary>
public sealed record TrustScoreComponent
{
/// <summary>
/// Component score (0-100).
/// </summary>
public required int Score { get; init; }
/// <summary>
/// Weight of this component in the total score (0.0-1.0).
/// </summary>
public required decimal Weight { get; init; }
/// <summary>
/// Weighted contribution to total score.
/// </summary>
public decimal Contribution => Score * Weight;
}
/// <summary>

View File

@@ -0,0 +1,245 @@
namespace StellaOps.Provcache;
/// <summary>
/// Manifest showing the exact inputs that form a VeriKey and cached decision.
/// Used for transparency and debugging to show what evidence contributed to a decision.
/// </summary>
public sealed record InputManifest
{
/// <summary>
/// The VeriKey this manifest describes.
/// </summary>
public required string VeriKey { get; init; }
/// <summary>
/// Information about the source artifact (container image, binary, etc.).
/// </summary>
public required SourceArtifactInfo SourceArtifact { get; init; }
/// <summary>
/// Information about the SBOM used in the decision.
/// </summary>
public required SbomInfo Sbom { get; init; }
/// <summary>
/// Information about VEX statements contributing to the decision.
/// </summary>
public required VexInfo Vex { get; init; }
/// <summary>
/// Information about the policy used in evaluation.
/// </summary>
public required PolicyInfo Policy { get; init; }
/// <summary>
/// Information about signers/attestors.
/// </summary>
public required SignerInfo Signers { get; init; }
/// <summary>
/// Time window information for cache validity.
/// </summary>
public required TimeWindowInfo TimeWindow { get; init; }
}
/// <summary>
/// Information about the source artifact.
/// </summary>
public sealed record SourceArtifactInfo
{
/// <summary>
/// Content-addressed hash of the artifact (e.g., sha256:abc123...).
/// </summary>
public required string Digest { get; init; }
/// <summary>
/// Type of artifact (container-image, binary, archive, etc.).
/// </summary>
public string? ArtifactType { get; init; }
/// <summary>
/// OCI reference if applicable (e.g., ghcr.io/org/repo:tag).
/// </summary>
public string? OciReference { get; init; }
/// <summary>
/// Size of the artifact in bytes.
/// </summary>
public long? SizeBytes { get; init; }
}
/// <summary>
/// Information about the SBOM.
/// </summary>
public sealed record SbomInfo
{
/// <summary>
/// Canonical hash of the SBOM content.
/// </summary>
public required string Hash { get; init; }
/// <summary>
/// SBOM format (spdx-2.3, cyclonedx-1.6, etc.).
/// </summary>
public string? Format { get; init; }
/// <summary>
/// Number of packages in the SBOM.
/// </summary>
public int? PackageCount { get; init; }
/// <summary>
/// Number of packages with license information.
/// </summary>
public int? PackagesWithLicense { get; init; }
/// <summary>
/// Completeness percentage (0-100).
/// </summary>
public int? CompletenessScore { get; init; }
/// <summary>
/// When the SBOM was created or last updated.
/// </summary>
public DateTimeOffset? CreatedAt { get; init; }
}
/// <summary>
/// Information about VEX statements.
/// </summary>
public sealed record VexInfo
{
/// <summary>
/// Hash of the sorted VEX statement set.
/// </summary>
public required string HashSetHash { get; init; }
/// <summary>
/// Number of VEX statements contributing to this decision.
/// </summary>
public int StatementCount { get; init; }
/// <summary>
/// Sources of VEX statements (vendor names, OpenVEX IDs, etc.).
/// </summary>
public IReadOnlyList<string> Sources { get; init; } = [];
/// <summary>
/// Most recent VEX statement timestamp.
/// </summary>
public DateTimeOffset? LatestStatementAt { get; init; }
/// <summary>
/// Individual statement hashes (for verification).
/// </summary>
public IReadOnlyList<string> StatementHashes { get; init; } = [];
}
/// <summary>
/// Information about the policy.
/// </summary>
public sealed record PolicyInfo
{
/// <summary>
/// Canonical hash of the policy bundle.
/// </summary>
public required string Hash { get; init; }
/// <summary>
/// Policy pack identifier.
/// </summary>
public string? PackId { get; init; }
/// <summary>
/// Policy version number.
/// </summary>
public int? Version { get; init; }
/// <summary>
/// When the policy was last updated.
/// </summary>
public DateTimeOffset? LastUpdatedAt { get; init; }
/// <summary>
/// Human-readable policy name.
/// </summary>
public string? Name { get; init; }
}
/// <summary>
/// Information about signers and attestors.
/// </summary>
public sealed record SignerInfo
{
/// <summary>
/// Hash of the sorted signer set.
/// </summary>
public required string SetHash { get; init; }
/// <summary>
/// Number of signers in the set.
/// </summary>
public int SignerCount { get; init; }
/// <summary>
/// Signer certificate information.
/// </summary>
public IReadOnlyList<SignerCertificate> Certificates { get; init; } = [];
}
/// <summary>
/// Information about a signer certificate.
/// </summary>
public sealed record SignerCertificate
{
/// <summary>
/// Subject of the certificate (e.g., CN=...).
/// </summary>
public string? Subject { get; init; }
/// <summary>
/// Certificate issuer.
/// </summary>
public string? Issuer { get; init; }
/// <summary>
/// Certificate serial number or fingerprint.
/// </summary>
public string? Fingerprint { get; init; }
/// <summary>
/// When the certificate expires.
/// </summary>
public DateTimeOffset? ExpiresAt { get; init; }
/// <summary>
/// Trust level (fulcio, self-signed, enterprise-ca, etc.).
/// </summary>
public string? TrustLevel { get; init; }
}
/// <summary>
/// Information about the time window used in VeriKey.
/// </summary>
public sealed record TimeWindowInfo
{
/// <summary>
/// The time window bucket identifier.
/// </summary>
public required string Bucket { get; init; }
/// <summary>
/// Start of the time window (UTC).
/// </summary>
public DateTimeOffset? StartsAt { get; init; }
/// <summary>
/// End of the time window (UTC).
/// </summary>
public DateTimeOffset? EndsAt { get; init; }
/// <summary>
/// Duration of the time window.
/// </summary>
public TimeSpan? Duration { get; init; }
}

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);

View File

@@ -0,0 +1,312 @@
// ----------------------------------------------------------------------------
// Copyright (c) 2025 StellaOps contributors. All rights reserved.
// SPDX-License-Identifier: AGPL-3.0-or-later
// ----------------------------------------------------------------------------
using System.Text.Json.Serialization;
namespace StellaOps.Provcache.Oci;
/// <summary>
/// Constants for the Provcache OCI attestation predicate type.
/// </summary>
public static class ProvcachePredicateTypes
{
/// <summary>
/// The predicate type URI for Provcache decision attestations.
/// Format: stella.ops/provcache@v1
/// </summary>
public const string ProvcacheV1 = "stella.ops/provcache@v1";
/// <summary>
/// OCI media type for the attestation payload.
/// </summary>
public const string MediaType = "application/vnd.stellaops.provcache.decision+json";
/// <summary>
/// Statement type for in-toto attestations.
/// </summary>
public const string InTotoStatementType = "https://in-toto.io/Statement/v1";
}
/// <summary>
/// In-toto statement wrapper for Provcache attestations following SLSA v1.0 format.
/// </summary>
public sealed record ProvcacheStatement
{
/// <summary>
/// Statement type, always "https://in-toto.io/Statement/v1".
/// </summary>
[JsonPropertyName("_type")]
public string Type { get; init; } = ProvcachePredicateTypes.InTotoStatementType;
/// <summary>
/// List of subjects (artifacts) this attestation applies to.
/// </summary>
[JsonPropertyName("subject")]
public required IReadOnlyList<ProvcacheSubject> Subject { get; init; }
/// <summary>
/// Predicate type URI: stella.ops/provcache@v1.
/// </summary>
[JsonPropertyName("predicateType")]
public string PredicateType { get; init; } = ProvcachePredicateTypes.ProvcacheV1;
/// <summary>
/// The predicate payload containing the decision digest details.
/// </summary>
[JsonPropertyName("predicate")]
public required ProvcachePredicate Predicate { get; init; }
}
/// <summary>
/// Subject representing the artifact (container image) this attestation applies to.
/// </summary>
public sealed record ProvcacheSubject
{
/// <summary>
/// Name of the artifact (e.g., container image reference without tag).
/// Example: ghcr.io/stellaops/scanner
/// </summary>
[JsonPropertyName("name")]
public required string Name { get; init; }
/// <summary>
/// Cryptographic digests of the artifact.
/// </summary>
[JsonPropertyName("digest")]
public required IReadOnlyDictionary<string, string> Digest { get; init; }
}
/// <summary>
/// The Provcache predicate containing decision digest and provenance metadata.
/// </summary>
public sealed record ProvcachePredicate
{
/// <summary>
/// Schema version of this predicate format.
/// </summary>
[JsonPropertyName("version")]
public string Version { get; init; } = "v1";
/// <summary>
/// Composite cache key (VeriKey) that uniquely identifies the decision context.
/// Format: sha256:&lt;hex&gt;
/// </summary>
[JsonPropertyName("veriKey")]
public required string VeriKey { get; init; }
/// <summary>
/// Hash of sorted dispositions from the evaluation result.
/// Format: sha256:&lt;hex&gt;
/// </summary>
[JsonPropertyName("verdictHash")]
public required string VerdictHash { get; init; }
/// <summary>
/// Merkle root of all evidence chunks used in this decision.
/// Format: sha256:&lt;hex&gt;
/// </summary>
[JsonPropertyName("proofRoot")]
public required string ProofRoot { get; init; }
/// <summary>
/// Composite trust score (0-100) indicating decision confidence.
/// </summary>
[JsonPropertyName("trustScore")]
public required int TrustScore { get; init; }
/// <summary>
/// Breakdown of trust score by component.
/// </summary>
[JsonPropertyName("trustScoreBreakdown")]
public ProvcacheTrustBreakdown? TrustScoreBreakdown { get; init; }
/// <summary>
/// Input manifest describing components used for the decision.
/// </summary>
[JsonPropertyName("inputManifest")]
public required ProvcacheInputSummary InputManifest { get; init; }
/// <summary>
/// Replay identifiers for deterministic re-evaluation.
/// </summary>
[JsonPropertyName("replaySeed")]
public required ProvcacheReplaySeed ReplaySeed { get; init; }
/// <summary>
/// UTC timestamp when this decision was made.
/// ISO 8601 format.
/// </summary>
[JsonPropertyName("createdAt")]
public required string CreatedAt { get; init; }
/// <summary>
/// UTC timestamp when this decision expires.
/// ISO 8601 format.
/// </summary>
[JsonPropertyName("expiresAt")]
public required string ExpiresAt { get; init; }
/// <summary>
/// Summary of verdicts included in this decision.
/// </summary>
[JsonPropertyName("verdictSummary")]
public ProvcacheVerdictSummary? VerdictSummary { get; init; }
}
/// <summary>
/// Trust score breakdown by evidence component.
/// </summary>
public sealed record ProvcacheTrustBreakdown
{
/// <summary>
/// Reachability evidence contribution (weight: 25%).
/// </summary>
[JsonPropertyName("reachability")]
public required ProvcacheTrustComponent Reachability { get; init; }
/// <summary>
/// SBOM completeness contribution (weight: 20%).
/// </summary>
[JsonPropertyName("sbomCompleteness")]
public required ProvcacheTrustComponent SbomCompleteness { get; init; }
/// <summary>
/// VEX coverage contribution (weight: 20%).
/// </summary>
[JsonPropertyName("vexCoverage")]
public required ProvcacheTrustComponent VexCoverage { get; init; }
/// <summary>
/// Policy freshness contribution (weight: 15%).
/// </summary>
[JsonPropertyName("policyFreshness")]
public required ProvcacheTrustComponent PolicyFreshness { get; init; }
/// <summary>
/// Signer trust contribution (weight: 20%).
/// </summary>
[JsonPropertyName("signerTrust")]
public required ProvcacheTrustComponent SignerTrust { get; init; }
}
/// <summary>
/// Individual trust score component.
/// </summary>
public sealed record ProvcacheTrustComponent
{
/// <summary>
/// Component score (0-100).
/// </summary>
[JsonPropertyName("score")]
public required int Score { get; init; }
/// <summary>
/// Weight of this component (0.0-1.0).
/// </summary>
[JsonPropertyName("weight")]
public required decimal Weight { get; init; }
}
/// <summary>
/// Summary of input components used for the decision.
/// </summary>
public sealed record ProvcacheInputSummary
{
/// <summary>
/// Hash of the source artifact.
/// </summary>
[JsonPropertyName("sourceHash")]
public required string SourceHash { get; init; }
/// <summary>
/// Hash of the SBOM used.
/// </summary>
[JsonPropertyName("sbomHash")]
public string? SbomHash { get; init; }
/// <summary>
/// Hash of the VEX set used.
/// </summary>
[JsonPropertyName("vexSetHash")]
public string? VexSetHash { get; init; }
/// <summary>
/// Hash of the policy used.
/// </summary>
[JsonPropertyName("policyHash")]
public string? PolicyHash { get; init; }
/// <summary>
/// Hash of the signer set.
/// </summary>
[JsonPropertyName("signerSetHash")]
public string? SignerSetHash { get; init; }
}
/// <summary>
/// Replay seed for deterministic re-evaluation.
/// </summary>
public sealed record ProvcacheReplaySeed
{
/// <summary>
/// Advisory feed identifiers used.
/// </summary>
[JsonPropertyName("feedIds")]
public required IReadOnlyList<string> FeedIds { get; init; }
/// <summary>
/// Policy rule identifiers used.
/// </summary>
[JsonPropertyName("ruleIds")]
public required IReadOnlyList<string> RuleIds { get; init; }
/// <summary>
/// Optional frozen epoch timestamp.
/// </summary>
[JsonPropertyName("frozenEpoch")]
public string? FrozenEpoch { get; init; }
}
/// <summary>
/// Summary of verdicts in the decision.
/// </summary>
public sealed record ProvcacheVerdictSummary
{
/// <summary>
/// Total number of findings evaluated.
/// </summary>
[JsonPropertyName("totalFindings")]
public required int TotalFindings { get; init; }
/// <summary>
/// Number of findings marked as affected.
/// </summary>
[JsonPropertyName("affected")]
public required int Affected { get; init; }
/// <summary>
/// Number of findings marked as not_affected.
/// </summary>
[JsonPropertyName("notAffected")]
public required int NotAffected { get; init; }
/// <summary>
/// Number of findings with mitigations applied.
/// </summary>
[JsonPropertyName("mitigated")]
public required int Mitigated { get; init; }
/// <summary>
/// Number of findings under investigation.
/// </summary>
[JsonPropertyName("underInvestigation")]
public required int UnderInvestigation { get; init; }
/// <summary>
/// Number of findings with known fixes.
/// </summary>
[JsonPropertyName("fixed")]
public required int Fixed { get; init; }
}

View File

@@ -331,6 +331,9 @@ public sealed class ProvcacheService : IProvcacheService
{
var stats = await _repository.GetStatisticsAsync(cancellationToken).ConfigureAwait(false);
// Update the items count gauge for Prometheus
ProvcacheTelemetry.SetItemsCount(stats.TotalEntries);
double avgLatency, p99Latency;
lock (_metricsLock)
{

View File

@@ -40,6 +40,7 @@ public static class ProvcacheTelemetry
private const string WriteBehindQueueSizeMetric = "provcache_writebehind_queue_size";
private const string LatencySecondsMetric = "provcache_latency_seconds";
private const string EntriesSizeMetric = "provcache_entry_bytes";
private const string ItemsCountMetric = "provcache_items_count";
#endregion
@@ -92,6 +93,7 @@ public static class ProvcacheTelemetry
#region Gauges
private static int _writeBehindQueueSize;
private static long _itemsCount;
/// <summary>
/// Observable gauge for write-behind queue size.
@@ -102,6 +104,15 @@ public static class ProvcacheTelemetry
unit: "items",
description: "Current write-behind queue size.");
/// <summary>
/// Observable gauge for total cache items count.
/// </summary>
public static readonly ObservableGauge<long> ItemsCountGauge = Meter.CreateObservableGauge(
ItemsCountMetric,
() => _itemsCount,
unit: "items",
description: "Current number of cached entries.");
/// <summary>
/// Update the write-behind queue size gauge.
/// </summary>
@@ -111,6 +122,15 @@ public static class ProvcacheTelemetry
_writeBehindQueueSize = size;
}
/// <summary>
/// Update the items count gauge.
/// </summary>
/// <param name="count">Current count of cache entries.</param>
public static void SetItemsCount(long count)
{
_itemsCount = count;
}
#endregion
#region Activity Tracing