semi implemented and features implemented save checkpoint

This commit is contained in:
master
2026-02-08 18:00:49 +02:00
parent 04360dff63
commit 1bf6bbf395
20895 changed files with 716795 additions and 64 deletions

View File

@@ -0,0 +1,43 @@
using System.Security.Cryptography;
namespace StellaOps.Scanner.Evidence;
/// <summary>
/// Creates deterministic idempotency keys for DSSE attestation payloads.
/// </summary>
public static class AttestationIdempotencyKey
{
/// <summary>
/// Computes a stable SHA-256 idempotency key for a DSSE envelope.
/// </summary>
public static string FromDsseEnvelope(ReadOnlySpan<byte> dsseEnvelopeBytes)
{
if (dsseEnvelopeBytes.IsEmpty)
{
throw new ArgumentException("DSSE envelope bytes cannot be empty.", nameof(dsseEnvelopeBytes));
}
var hash = SHA256.HashData(dsseEnvelopeBytes);
return $"sha256:{Convert.ToHexStringLower(hash)}";
}
/// <summary>
/// Converts an idempotency key into a stable OCI-safe tag.
/// </summary>
public static string ToOciTag(string idempotencyKey, string prefix = "verdict")
{
ArgumentException.ThrowIfNullOrWhiteSpace(idempotencyKey);
var normalized = idempotencyKey.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase)
? idempotencyKey[7..]
: idempotencyKey;
var compact = normalized.Trim().ToLowerInvariant();
if (compact.Length > 48)
{
compact = compact[..48];
}
return $"{prefix}-{compact}";
}
}

View File

@@ -6,3 +6,4 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| --- | --- | --- |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/Scanner/__Libraries/StellaOps.Scanner.Evidence/StellaOps.Scanner.Evidence.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
| SPRINT-20260208-060-IDEMP-001 | DONE | Implement idempotent verdict attestation submission (idempotency key + dedupe + retry classification + tests). |

View File

@@ -0,0 +1,8 @@
namespace StellaOps.Scanner.Storage.Oci;
public interface IVerdictOciPublisher
{
Task<OciArtifactPushResult> PushAsync(
VerdictOciPublishRequest request,
CancellationToken cancellationToken = default);
}

View File

@@ -14,6 +14,7 @@ public static class OciAnnotations
public const string StellaAfterDigest = "org.stellaops.delta.after.digest";
public const string StellaSbomDigest = "org.stellaops.sbom.digest";
public const string StellaVerdictDigest = "org.stellaops.verdict.digest";
public const string StellaIdempotencyKey = "org.stellaops.idempotency.key";
// Sprint: SPRINT_4300_0001_0001 - OCI Verdict Attestation Push
/// <summary>

View File

@@ -60,16 +60,14 @@ public sealed class OciArtifactPusher
try
{
var configDigest = await PushBlobAsync(reference, EmptyConfigBlob, OciMediaTypes.EmptyConfig, auth, cancellationToken)
.ConfigureAwait(false);
var configDigest = ComputeDigest(EmptyConfigBlob);
var layerDescriptors = new List<OciDescriptor>();
var layerDigests = new List<string>();
foreach (var layer in request.Layers)
{
var digest = await PushBlobAsync(reference, layer.Content, layer.MediaType, auth, cancellationToken)
.ConfigureAwait(false);
var digest = ComputeDigest(layer.Content);
layerDescriptors.Add(new OciDescriptor
{
@@ -86,7 +84,45 @@ public sealed class OciArtifactPusher
var manifestBytes = JsonSerializer.SerializeToUtf8Bytes(manifest, SerializerOptions);
var manifestDigest = ComputeDigest(manifestBytes);
var tag = reference.Tag ?? manifestDigest.Replace("sha256:", string.Empty, StringComparison.Ordinal);
var tag = request.Tag;
if (string.IsNullOrWhiteSpace(tag))
{
tag = reference.Tag ?? manifestDigest.Replace("sha256:", string.Empty, StringComparison.Ordinal);
}
if (request.SkipIfTagExists)
{
var existingDigest = await TryGetExistingManifestDigestAsync(reference, tag, auth, cancellationToken)
.ConfigureAwait(false);
if (existingDigest is not null)
{
if (string.IsNullOrWhiteSpace(existingDigest))
{
existingDigest = manifestDigest;
}
var existingReference = $"{reference.Registry}/{reference.Repository}@{existingDigest}";
_logger.LogInformation("OCI artifact already exists for tag {Tag}: {Reference}", tag, existingReference);
return new OciArtifactPushResult
{
Success = true,
AlreadyExists = true,
ManifestDigest = existingDigest,
ManifestReference = existingReference,
LayerDigests = layerDigests
};
}
}
await PushBlobAsync(reference, EmptyConfigBlob, OciMediaTypes.EmptyConfig, auth, cancellationToken)
.ConfigureAwait(false);
foreach (var layer in request.Layers)
{
await PushBlobAsync(reference, layer.Content, layer.MediaType, auth, cancellationToken)
.ConfigureAwait(false);
}
await PushManifestAsync(reference, manifestBytes, tag, auth, cancellationToken).ConfigureAwait(false);
var manifestReference = $"{reference.Registry}/{reference.Repository}@{manifestDigest}";
@@ -94,11 +130,12 @@ public sealed class OciArtifactPusher
_logger.LogInformation("Pushed OCI artifact {Reference}", manifestReference);
return new OciArtifactPushResult
{
Success = true,
ManifestDigest = manifestDigest,
ManifestReference = manifestReference,
LayerDigests = layerDigests
{
Success = true,
AlreadyExists = false,
ManifestDigest = manifestDigest,
ManifestReference = manifestReference,
LayerDigests = layerDigests
};
}
catch (OciRegistryException ex)
@@ -113,6 +150,40 @@ public sealed class OciArtifactPusher
}
}
private async Task<string?> TryGetExistingManifestDigestAsync(
OciImageReference reference,
string tag,
OciRegistryAuthorization auth,
CancellationToken cancellationToken)
{
var manifestUri = BuildRegistryUri(reference, $"manifests/{tag}");
using var request = new HttpRequestMessage(HttpMethod.Head, manifestUri);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(OciMediaTypes.ImageManifest));
auth.ApplyTo(request);
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
if (response.StatusCode == HttpStatusCode.NotFound)
{
return null;
}
if (!response.IsSuccessStatusCode)
{
throw new OciRegistryException($"Manifest HEAD failed with {response.StatusCode}", "ERR_OCI_MANIFEST_HEAD");
}
if (response.Headers.TryGetValues("Docker-Content-Digest", out var digestValues))
{
var digest = digestValues.FirstOrDefault();
if (!string.IsNullOrWhiteSpace(digest))
{
return digest;
}
}
return string.Empty;
}
private OciArtifactManifest BuildManifest(
OciArtifactPushRequest request,
string configDigest,

View File

@@ -57,12 +57,15 @@ public sealed record OciArtifactPushRequest
public required string ArtifactType { get; init; }
public required IReadOnlyList<OciLayerContent> Layers { get; init; }
public string? SubjectDigest { get; init; }
public string? Tag { get; init; }
public bool SkipIfTagExists { get; init; } = true;
public IReadOnlyDictionary<string, string>? Annotations { get; init; }
}
public sealed record OciArtifactPushResult
{
public required bool Success { get; init; }
public bool AlreadyExists { get; init; }
public string? ManifestDigest { get; init; }
public string? ManifestReference { get; init; }
public IReadOnlyList<string>? LayerDigests { get; init; }

View File

@@ -6,3 +6,4 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| --- | --- | --- |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/Scanner/__Libraries/StellaOps.Scanner.Storage.Oci/StellaOps.Scanner.Storage.Oci.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
| SPRINT-20260208-060-IDEMP-001 | DONE | Implement idempotent verdict attestation submission (idempotency key + dedupe + retry classification + tests). |

View File

@@ -7,6 +7,7 @@
using StellaOps.Scanner.Storage.Oci.Diagnostics;
using StellaOps.Scanner.Evidence;
using System.Diagnostics;
using System.Globalization;
@@ -68,6 +69,11 @@ public sealed record VerdictOciPublishRequest
/// </summary>
public string? AttestationDigest { get; init; }
/// <summary>
/// Optional idempotency key. When omitted, computed from DSSE envelope bytes.
/// </summary>
public string? IdempotencyKey { get; init; }
/// <summary>
/// When the verdict was computed.
/// </summary>
@@ -90,7 +96,7 @@ public sealed record VerdictOciPublishRequest
/// Service for pushing risk verdict attestations as OCI referrer artifacts.
/// This enables verdicts to be portable "ship tokens" attached to container images.
/// </summary>
public sealed class VerdictOciPublisher
public sealed class VerdictOciPublisher : IVerdictOciPublisher
{
private readonly OciArtifactPusher _pusher;
@@ -128,13 +134,19 @@ public sealed class VerdictOciPublisher
try
{
var idempotencyKey = string.IsNullOrWhiteSpace(request.IdempotencyKey)
? AttestationIdempotencyKey.FromDsseEnvelope(request.DsseEnvelopeBytes)
: request.IdempotencyKey!;
var manifestTag = AttestationIdempotencyKey.ToOciTag(idempotencyKey);
var annotations = new Dictionary<string, string>(StringComparer.Ordinal)
{
[OciAnnotations.StellaPredicateType] = VerdictPredicateTypes.Verdict,
[OciAnnotations.StellaSbomDigest] = request.SbomDigest,
[OciAnnotations.StellaFeedsDigest] = request.FeedsDigest,
[OciAnnotations.StellaPolicyDigest] = request.PolicyDigest,
[OciAnnotations.StellaVerdictDecision] = request.Decision
[OciAnnotations.StellaVerdictDecision] = request.Decision,
[OciAnnotations.StellaIdempotencyKey] = idempotencyKey
};
if (!string.IsNullOrWhiteSpace(request.GraphRevisionId))
@@ -173,6 +185,8 @@ public sealed class VerdictOciPublisher
Reference = request.Reference,
ArtifactType = OciMediaTypes.VerdictAttestation,
SubjectDigest = request.ImageDigest,
Tag = manifestTag,
SkipIfTagExists = true,
Layers =
[
new OciLayerContent