feat: Initialize Zastava Webhook service with TLS and Authority authentication

- Added Program.cs to set up the web application with Serilog for logging, health check endpoints, and a placeholder admission endpoint.
- Configured Kestrel server to use TLS 1.3 and handle client certificates appropriately.
- Created StellaOps.Zastava.Webhook.csproj with necessary dependencies including Serilog and Polly.
- Documented tasks in TASKS.md for the Zastava Webhook project, outlining current work and exit criteria for each task.
This commit is contained in:
master
2025-10-19 18:36:22 +03:00
parent 2062da7a8b
commit d099a90f9b
966 changed files with 91038 additions and 1850 deletions

View File

@@ -0,0 +1,33 @@
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Attestor.Core.Storage;
namespace StellaOps.Attestor.Infrastructure.Storage;
internal sealed class InMemoryAttestorDedupeStore : IAttestorDedupeStore
{
private readonly ConcurrentDictionary<string, (string Uuid, DateTimeOffset ExpiresAt)> _store = new();
public Task<string?> TryGetExistingAsync(string bundleSha256, CancellationToken cancellationToken = default)
{
if (_store.TryGetValue(bundleSha256, out var entry))
{
if (entry.ExpiresAt > DateTimeOffset.UtcNow)
{
return Task.FromResult<string?>(entry.Uuid);
}
_store.TryRemove(bundleSha256, out _);
}
return Task.FromResult<string?>(null);
}
public Task SetAsync(string bundleSha256, string rekorUuid, TimeSpan ttl, CancellationToken cancellationToken = default)
{
_store[bundleSha256] = (rekorUuid, DateTimeOffset.UtcNow.Add(ttl));
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,115 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;
using StellaOps.Attestor.Core.Audit;
using StellaOps.Attestor.Core.Storage;
namespace StellaOps.Attestor.Infrastructure.Storage;
internal sealed class MongoAttestorAuditSink : IAttestorAuditSink
{
private readonly IMongoCollection<AttestorAuditDocument> _collection;
public MongoAttestorAuditSink(IMongoCollection<AttestorAuditDocument> collection)
{
_collection = collection;
}
public Task WriteAsync(AttestorAuditRecord record, CancellationToken cancellationToken = default)
{
var document = AttestorAuditDocument.FromRecord(record);
return _collection.InsertOneAsync(document, cancellationToken: cancellationToken);
}
internal sealed class AttestorAuditDocument
{
[BsonId]
public ObjectId Id { get; set; }
[BsonElement("ts")]
public BsonDateTime Timestamp { get; set; } = BsonDateTime.Create(DateTime.UtcNow);
[BsonElement("action")]
public string Action { get; set; } = string.Empty;
[BsonElement("result")]
public string Result { get; set; } = string.Empty;
[BsonElement("rekorUuid")]
public string? RekorUuid { get; set; }
[BsonElement("index")]
public long? Index { get; set; }
[BsonElement("artifactSha256")]
public string ArtifactSha256 { get; set; } = string.Empty;
[BsonElement("bundleSha256")]
public string BundleSha256 { get; set; } = string.Empty;
[BsonElement("backend")]
public string Backend { get; set; } = string.Empty;
[BsonElement("latencyMs")]
public long LatencyMs { get; set; }
[BsonElement("caller")]
public CallerDocument Caller { get; set; } = new();
[BsonElement("metadata")]
public BsonDocument Metadata { get; set; } = new();
public static AttestorAuditDocument FromRecord(AttestorAuditRecord record)
{
var metadata = new BsonDocument();
foreach (var kvp in record.Metadata)
{
metadata[kvp.Key] = kvp.Value;
}
return new AttestorAuditDocument
{
Id = ObjectId.GenerateNewId(),
Timestamp = BsonDateTime.Create(record.Timestamp.UtcDateTime),
Action = record.Action,
Result = record.Result,
RekorUuid = record.RekorUuid,
Index = record.Index,
ArtifactSha256 = record.ArtifactSha256,
BundleSha256 = record.BundleSha256,
Backend = record.Backend,
LatencyMs = record.LatencyMs,
Caller = new CallerDocument
{
Subject = record.Caller.Subject,
Audience = record.Caller.Audience,
ClientId = record.Caller.ClientId,
MtlsThumbprint = record.Caller.MtlsThumbprint,
Tenant = record.Caller.Tenant
},
Metadata = metadata
};
}
internal sealed class CallerDocument
{
[BsonElement("subject")]
public string? Subject { get; set; }
[BsonElement("audience")]
public string? Audience { get; set; }
[BsonElement("clientId")]
public string? ClientId { get; set; }
[BsonElement("mtlsThumbprint")]
public string? MtlsThumbprint { get; set; }
[BsonElement("tenant")]
public string? Tenant { get; set; }
}
}
}

View File

@@ -0,0 +1,245 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;
using StellaOps.Attestor.Core.Storage;
namespace StellaOps.Attestor.Infrastructure.Storage;
internal sealed class MongoAttestorEntryRepository : IAttestorEntryRepository
{
private readonly IMongoCollection<AttestorEntryDocument> _entries;
public MongoAttestorEntryRepository(IMongoCollection<AttestorEntryDocument> entries)
{
_entries = entries;
}
public async Task<AttestorEntry?> GetByBundleShaAsync(string bundleSha256, CancellationToken cancellationToken = default)
{
var filter = Builders<AttestorEntryDocument>.Filter.Eq(x => x.BundleSha256, bundleSha256);
var document = await _entries.Find(filter).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
return document?.ToDomain();
}
public async Task<AttestorEntry?> GetByUuidAsync(string rekorUuid, CancellationToken cancellationToken = default)
{
var filter = Builders<AttestorEntryDocument>.Filter.Eq(x => x.Id, rekorUuid);
var document = await _entries.Find(filter).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
return document?.ToDomain();
}
public async Task<IReadOnlyList<AttestorEntry>> GetByArtifactShaAsync(string artifactSha256, CancellationToken cancellationToken = default)
{
var filter = Builders<AttestorEntryDocument>.Filter.Eq(x => x.Artifact.Sha256, artifactSha256);
var documents = await _entries.Find(filter).ToListAsync(cancellationToken).ConfigureAwait(false);
return documents.ConvertAll(static doc => doc.ToDomain());
}
public async Task SaveAsync(AttestorEntry entry, CancellationToken cancellationToken = default)
{
var document = AttestorEntryDocument.FromDomain(entry);
var filter = Builders<AttestorEntryDocument>.Filter.Eq(x => x.Id, document.Id);
await _entries.ReplaceOneAsync(filter, document, new ReplaceOptions { IsUpsert = true }, cancellationToken).ConfigureAwait(false);
}
[BsonIgnoreExtraElements]
internal sealed class AttestorEntryDocument
{
[BsonId]
public string Id { get; set; } = string.Empty;
[BsonElement("artifact")]
public ArtifactDocument Artifact { get; set; } = new();
[BsonElement("bundleSha256")]
public string BundleSha256 { get; set; } = string.Empty;
[BsonElement("index")]
public long? Index { get; set; }
[BsonElement("proof")]
public ProofDocument? Proof { get; set; }
[BsonElement("log")]
public LogDocument Log { get; set; } = new();
[BsonElement("createdAt")]
public BsonDateTime CreatedAt { get; set; } = BsonDateTime.Create(System.DateTimeOffset.UtcNow);
[BsonElement("status")]
public string Status { get; set; } = "pending";
[BsonElement("signerIdentity")]
public SignerIdentityDocument SignerIdentity { get; set; } = new();
public static AttestorEntryDocument FromDomain(AttestorEntry entry)
{
return new AttestorEntryDocument
{
Id = entry.RekorUuid,
Artifact = new ArtifactDocument
{
Sha256 = entry.Artifact.Sha256,
Kind = entry.Artifact.Kind,
ImageDigest = entry.Artifact.ImageDigest,
SubjectUri = entry.Artifact.SubjectUri
},
BundleSha256 = entry.BundleSha256,
Index = entry.Index,
Proof = entry.Proof is null ? null : new ProofDocument
{
Checkpoint = entry.Proof.Checkpoint is null ? null : new CheckpointDocument
{
Origin = entry.Proof.Checkpoint.Origin,
Size = entry.Proof.Checkpoint.Size,
RootHash = entry.Proof.Checkpoint.RootHash,
Timestamp = entry.Proof.Checkpoint.Timestamp is null
? null
: BsonDateTime.Create(entry.Proof.Checkpoint.Timestamp.Value)
},
Inclusion = entry.Proof.Inclusion is null ? null : new InclusionDocument
{
LeafHash = entry.Proof.Inclusion.LeafHash,
Path = entry.Proof.Inclusion.Path
}
},
Log = new LogDocument
{
Url = entry.Log.Url,
LogId = entry.Log.LogId
},
CreatedAt = BsonDateTime.Create(entry.CreatedAt.UtcDateTime),
Status = entry.Status,
SignerIdentity = new SignerIdentityDocument
{
Mode = entry.SignerIdentity.Mode,
Issuer = entry.SignerIdentity.Issuer,
SubjectAlternativeName = entry.SignerIdentity.SubjectAlternativeName,
KeyId = entry.SignerIdentity.KeyId
}
};
}
public AttestorEntry ToDomain()
{
return new AttestorEntry
{
RekorUuid = Id,
Artifact = new AttestorEntry.ArtifactDescriptor
{
Sha256 = Artifact.Sha256,
Kind = Artifact.Kind,
ImageDigest = Artifact.ImageDigest,
SubjectUri = Artifact.SubjectUri
},
BundleSha256 = BundleSha256,
Index = Index,
Proof = Proof is null ? null : new AttestorEntry.ProofDescriptor
{
Checkpoint = Proof.Checkpoint is null ? null : new AttestorEntry.CheckpointDescriptor
{
Origin = Proof.Checkpoint.Origin,
Size = Proof.Checkpoint.Size,
RootHash = Proof.Checkpoint.RootHash,
Timestamp = Proof.Checkpoint.Timestamp?.ToUniversalTime()
},
Inclusion = Proof.Inclusion is null ? null : new AttestorEntry.InclusionDescriptor
{
LeafHash = Proof.Inclusion.LeafHash,
Path = Proof.Inclusion.Path
}
},
Log = new AttestorEntry.LogDescriptor
{
Url = Log.Url,
LogId = Log.LogId
},
CreatedAt = CreatedAt.ToUniversalTime(),
Status = Status,
SignerIdentity = new AttestorEntry.SignerIdentityDescriptor
{
Mode = SignerIdentity.Mode,
Issuer = SignerIdentity.Issuer,
SubjectAlternativeName = SignerIdentity.SubjectAlternativeName,
KeyId = SignerIdentity.KeyId
}
};
}
internal sealed class ArtifactDocument
{
[BsonElement("sha256")]
public string Sha256 { get; set; } = string.Empty;
[BsonElement("kind")]
public string Kind { get; set; } = string.Empty;
[BsonElement("imageDigest")]
public string? ImageDigest { get; set; }
[BsonElement("subjectUri")]
public string? SubjectUri { get; set; }
}
internal sealed class ProofDocument
{
[BsonElement("checkpoint")]
public CheckpointDocument? Checkpoint { get; set; }
[BsonElement("inclusion")]
public InclusionDocument? Inclusion { get; set; }
}
internal sealed class CheckpointDocument
{
[BsonElement("origin")]
public string? Origin { get; set; }
[BsonElement("size")]
public long Size { get; set; }
[BsonElement("rootHash")]
public string? RootHash { get; set; }
[BsonElement("timestamp")]
public BsonDateTime? Timestamp { get; set; }
}
internal sealed class InclusionDocument
{
[BsonElement("leafHash")]
public string? LeafHash { get; set; }
[BsonElement("path")]
public IReadOnlyList<string> Path { get; set; } = System.Array.Empty<string>();
}
internal sealed class LogDocument
{
[BsonElement("url")]
public string Url { get; set; } = string.Empty;
[BsonElement("logId")]
public string? LogId { get; set; }
}
internal sealed class SignerIdentityDocument
{
[BsonElement("mode")]
public string Mode { get; set; } = string.Empty;
[BsonElement("issuer")]
public string? Issuer { get; set; }
[BsonElement("san")]
public string? SubjectAlternativeName { get; set; }
[BsonElement("kid")]
public string? KeyId { get; set; }
}
}
}

View File

@@ -0,0 +1,22 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using StellaOps.Attestor.Core.Storage;
namespace StellaOps.Attestor.Infrastructure.Storage;
internal sealed class NullAttestorArchiveStore : IAttestorArchiveStore
{
private readonly ILogger<NullAttestorArchiveStore> _logger;
public NullAttestorArchiveStore(ILogger<NullAttestorArchiveStore> logger)
{
_logger = logger;
}
public Task ArchiveBundleAsync(AttestorArchiveBundle bundle, CancellationToken cancellationToken = default)
{
_logger.LogDebug("Archive disabled; skipping bundle {BundleSha}", bundle.BundleSha256);
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,34 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using StackExchange.Redis;
using StellaOps.Attestor.Core.Options;
using StellaOps.Attestor.Core.Storage;
namespace StellaOps.Attestor.Infrastructure.Storage;
internal sealed class RedisAttestorDedupeStore : IAttestorDedupeStore
{
private readonly IDatabase _database;
private readonly string _prefix;
public RedisAttestorDedupeStore(IConnectionMultiplexer multiplexer, IOptions<AttestorOptions> options)
{
_database = multiplexer.GetDatabase();
_prefix = options.Value.Redis.DedupePrefix ?? "attestor:dedupe:";
}
public async Task<string?> TryGetExistingAsync(string bundleSha256, CancellationToken cancellationToken = default)
{
var value = await _database.StringGetAsync(BuildKey(bundleSha256)).ConfigureAwait(false);
return value.HasValue ? value.ToString() : null;
}
public Task SetAsync(string bundleSha256, string rekorUuid, TimeSpan ttl, CancellationToken cancellationToken = default)
{
return _database.StringSetAsync(BuildKey(bundleSha256), rekorUuid, ttl);
}
private RedisKey BuildKey(string bundleSha256) => new RedisKey(_prefix + bundleSha256);
}

View File

@@ -0,0 +1,72 @@
using System;
using System.IO;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Amazon.S3;
using Amazon.S3.Model;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Attestor.Core.Options;
using StellaOps.Attestor.Core.Storage;
namespace StellaOps.Attestor.Infrastructure.Storage;
internal sealed class S3AttestorArchiveStore : IAttestorArchiveStore, IDisposable
{
private readonly IAmazonS3 _s3;
private readonly AttestorOptions.S3Options _options;
private readonly ILogger<S3AttestorArchiveStore> _logger;
private bool _disposed;
public S3AttestorArchiveStore(IAmazonS3 s3, IOptions<AttestorOptions> options, ILogger<S3AttestorArchiveStore> logger)
{
_s3 = s3;
_options = options.Value.S3;
_logger = logger;
}
public async Task ArchiveBundleAsync(AttestorArchiveBundle bundle, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(_options.Bucket))
{
_logger.LogWarning("S3 archive bucket is not configured; skipping archive for bundle {Bundle}", bundle.BundleSha256);
return;
}
var prefix = _options.Prefix ?? "attest/";
await PutObjectAsync(prefix + "dsse/" + bundle.BundleSha256 + ".json", bundle.CanonicalBundleJson, cancellationToken).ConfigureAwait(false);
if (bundle.ProofJson.Length > 0)
{
await PutObjectAsync(prefix + "proof/" + bundle.RekorUuid + ".json", bundle.ProofJson, cancellationToken).ConfigureAwait(false);
}
var metadataObject = JsonSerializer.SerializeToUtf8Bytes(bundle.Metadata);
await PutObjectAsync(prefix + "meta/" + bundle.RekorUuid + ".json", metadataObject, cancellationToken).ConfigureAwait(false);
}
private Task PutObjectAsync(string key, byte[] content, CancellationToken cancellationToken)
{
using var stream = new MemoryStream(content);
var request = new PutObjectRequest
{
BucketName = _options.Bucket,
Key = key,
InputStream = stream,
AutoCloseStream = false
};
return _s3.PutObjectAsync(request, cancellationToken);
}
public void Dispose()
{
if (_disposed)
{
return;
}
_s3.Dispose();
_disposed = true;
}
}