using System.Collections.Concurrent; using System.Security.Cryptography; using System.Text; using System.Text.Json; using StellaOps.Policy.Registry.Contracts; using StellaOps.Policy.Registry.Storage; namespace StellaOps.Policy.Registry.Services; /// /// Default implementation of publish pipeline service. /// Handles policy pack publication with attestation generation. /// public sealed class PublishPipelineService : IPublishPipelineService { private const string BuilderId = "https://stellaops.io/policy-registry/v1"; private const string BuildType = "https://stellaops.io/policy-registry/v1/publish"; private const string AttestationPredicateType = "https://slsa.dev/provenance/v1"; private readonly IPolicyPackStore _packStore; private readonly IPolicyPackCompiler _compiler; private readonly IReviewWorkflowService _reviewService; private readonly TimeProvider _timeProvider; private readonly ConcurrentDictionary<(Guid TenantId, Guid PackId), PublicationStatus> _publications = new(); private readonly ConcurrentDictionary<(Guid TenantId, Guid PackId), PolicyPackAttestation> _attestations = new(); public PublishPipelineService( IPolicyPackStore packStore, IPolicyPackCompiler compiler, IReviewWorkflowService reviewService, TimeProvider? timeProvider = null) { _packStore = packStore ?? throw new ArgumentNullException(nameof(packStore)); _compiler = compiler ?? throw new ArgumentNullException(nameof(compiler)); _reviewService = reviewService ?? throw new ArgumentNullException(nameof(reviewService)); _timeProvider = timeProvider ?? TimeProvider.System; } public async Task PublishAsync( Guid tenantId, Guid packId, PublishPackRequest request, CancellationToken cancellationToken = default) { // Get the policy pack var pack = await _packStore.GetByIdAsync(tenantId, packId, cancellationToken); if (pack is null) { return new PublishResult { Success = false, Error = $"Policy pack {packId} not found" }; } // Verify pack is in correct state if (pack.Status != PolicyPackStatus.PendingReview) { return new PublishResult { Success = false, Error = $"Policy pack must be in PendingReview status to publish. Current status: {pack.Status}" }; } // Compile to get digest var compilationResult = await _compiler.CompileAsync(tenantId, packId, cancellationToken); if (!compilationResult.Success) { return new PublishResult { Success = false, Error = "Policy pack compilation failed. Cannot publish." }; } var now = _timeProvider.GetUtcNow(); var digest = compilationResult.Digest!; // Get review information if available var reviews = await _reviewService.ListReviewsAsync(tenantId, ReviewStatus.Approved, packId, 1, null, cancellationToken); var review = reviews.Items.FirstOrDefault(); // Build attestation var attestation = BuildAttestation( pack, digest, compilationResult, review, request, now); // Update pack status to Published var updatedPack = await _packStore.UpdateStatusAsync(tenantId, packId, PolicyPackStatus.Published, request.PublishedBy, cancellationToken); if (updatedPack is null) { return new PublishResult { Success = false, Error = "Failed to update policy pack status" }; } // Create publication status var status = new PublicationStatus { PackId = packId, PackVersion = pack.Version, Digest = digest, State = PublishState.Published, PublishedAt = now, PublishedBy = request.PublishedBy, SignatureKeyId = request.SigningOptions?.KeyId, SignatureAlgorithm = request.SigningOptions?.Algorithm }; _publications[(tenantId, packId)] = status; _attestations[(tenantId, packId)] = attestation; return new PublishResult { Success = true, PackId = packId, Digest = digest, Status = status, Attestation = attestation }; } public Task GetPublicationStatusAsync( Guid tenantId, Guid packId, CancellationToken cancellationToken = default) { _publications.TryGetValue((tenantId, packId), out var status); return Task.FromResult(status); } public Task GetAttestationAsync( Guid tenantId, Guid packId, CancellationToken cancellationToken = default) { _attestations.TryGetValue((tenantId, packId), out var attestation); return Task.FromResult(attestation); } public async Task VerifyAttestationAsync( Guid tenantId, Guid packId, CancellationToken cancellationToken = default) { var checks = new List(); var errors = new List(); var warnings = new List(); // Check publication exists if (!_publications.TryGetValue((tenantId, packId), out var status)) { return new AttestationVerificationResult { Valid = false, Errors = ["Policy pack is not published"] }; } checks.Add(new VerificationCheck { Name = "publication_exists", Passed = true, Details = $"Published at {status.PublishedAt:O}" }); // Check not revoked if (status.State == PublishState.Revoked) { errors.Add($"Policy pack was revoked at {status.RevokedAt:O}: {status.RevokeReason}"); checks.Add(new VerificationCheck { Name = "not_revoked", Passed = false, Details = status.RevokeReason }); } else { checks.Add(new VerificationCheck { Name = "not_revoked", Passed = true, Details = "Policy pack has not been revoked" }); } // Check attestation exists if (!_attestations.TryGetValue((tenantId, packId), out var attestation)) { errors.Add("Attestation not found"); checks.Add(new VerificationCheck { Name = "attestation_exists", Passed = false, Details = "No attestation on record" }); } else { checks.Add(new VerificationCheck { Name = "attestation_exists", Passed = true, Details = $"Found {attestation.Signatures.Count} signature(s)" }); // Verify signatures foreach (var sig in attestation.Signatures) { // In a real implementation, this would verify the actual cryptographic signature var sigValid = !string.IsNullOrEmpty(sig.Signature); checks.Add(new VerificationCheck { Name = $"signature_{sig.KeyId}", Passed = sigValid, Details = sigValid ? $"Signature verified for key {sig.KeyId}" : "Invalid signature" }); if (!sigValid) { errors.Add($"Invalid signature for key {sig.KeyId}"); } } } // Verify pack still exists and matches digest var pack = await _packStore.GetByIdAsync(tenantId, packId, cancellationToken); if (pack is null) { errors.Add("Policy pack no longer exists"); checks.Add(new VerificationCheck { Name = "pack_exists", Passed = false, Details = "Policy pack has been deleted" }); } else { checks.Add(new VerificationCheck { Name = "pack_exists", Passed = true, Details = $"Pack version: {pack.Version}" }); // Verify digest matches var digestMatch = pack.Digest == status.Digest; checks.Add(new VerificationCheck { Name = "digest_match", Passed = digestMatch, Details = digestMatch ? "Digest matches" : $"Digest mismatch: expected {status.Digest}, got {pack.Digest}" }); if (!digestMatch) { errors.Add("Policy pack has been modified since publication"); } } return new AttestationVerificationResult { Valid = errors.Count == 0, Checks = checks, Errors = errors.Count > 0 ? errors : null, Warnings = warnings.Count > 0 ? warnings : null }; } public Task ListPublishedAsync( Guid tenantId, int pageSize = 20, string? pageToken = null, CancellationToken cancellationToken = default) { var items = _publications .Where(kv => kv.Key.TenantId == tenantId) .Select(kv => kv.Value) .OrderByDescending(p => p.PublishedAt) .ToList(); int skip = 0; if (!string.IsNullOrEmpty(pageToken) && int.TryParse(pageToken, out var offset)) { skip = offset; } var pagedItems = items.Skip(skip).Take(pageSize).ToList(); string? nextToken = skip + pagedItems.Count < items.Count ? (skip + pagedItems.Count).ToString() : null; return Task.FromResult(new PublishedPackList { Items = pagedItems, NextPageToken = nextToken, TotalCount = items.Count }); } public async Task RevokeAsync( Guid tenantId, Guid packId, RevokePackRequest request, CancellationToken cancellationToken = default) { if (!_publications.TryGetValue((tenantId, packId), out var status)) { return new RevokeResult { Success = false, Error = "Policy pack is not published" }; } if (status.State == PublishState.Revoked) { return new RevokeResult { Success = false, Error = "Policy pack is already revoked" }; } var now = _timeProvider.GetUtcNow(); var updatedStatus = status with { State = PublishState.Revoked, RevokedAt = now, RevokedBy = request.RevokedBy, RevokeReason = request.Reason }; _publications[(tenantId, packId)] = updatedStatus; // Update pack status to archived await _packStore.UpdateStatusAsync(tenantId, packId, PolicyPackStatus.Archived, request.RevokedBy, cancellationToken); return new RevokeResult { Success = true, Status = updatedStatus }; } private PolicyPackAttestation BuildAttestation( PolicyPackEntity pack, string digest, PolicyPackCompilationResult compilationResult, ReviewRequest? review, PublishPackRequest request, DateTimeOffset now) { var subject = new AttestationSubject { Name = $"policy-pack/{pack.Name}", Digest = new Dictionary { ["sha256"] = digest.Replace("sha256:", "") } }; var predicate = new AttestationPredicate { BuildType = BuildType, Builder = new AttestationBuilder { Id = BuilderId, Version = "1.0.0" }, BuildStartedOn = pack.CreatedAt, BuildFinishedOn = now, Compilation = new PolicyPackCompilationMetadata { Digest = digest, RuleCount = compilationResult.Statistics?.TotalRules ?? 0, CompiledAt = now, Statistics = compilationResult.Statistics?.SeverityCounts }, Review = review is not null ? new PolicyPackReviewMetadata { ReviewId = review.ReviewId, ApprovedAt = review.ResolvedAt ?? now, ApprovedBy = review.ResolvedBy, Reviewers = review.Reviewers } : null, Metadata = request.Metadata?.ToDictionary(kv => kv.Key, kv => (object)kv.Value) }; var payload = new AttestationPayload { Type = "https://in-toto.io/Statement/v1", PredicateType = request.AttestationOptions?.PredicateType ?? AttestationPredicateType, Subject = subject, Predicate = predicate }; var payloadJson = JsonSerializer.Serialize(payload, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, WriteIndented = false }); var payloadBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(payloadJson)); // Generate signature (simulated - in production would use actual signing) var signature = GenerateSignature(payloadBase64, request.SigningOptions); return new PolicyPackAttestation { PayloadType = "application/vnd.in-toto+json", Payload = payloadBase64, Signatures = [ new AttestationSignature { KeyId = request.SigningOptions?.KeyId ?? "default", Signature = signature, Timestamp = request.SigningOptions?.IncludeTimestamp == true ? now : null } ] }; } private static string GenerateSignature(string payload, SigningOptions? options) { // In production, this would use actual cryptographic signing // For now, we generate a deterministic mock signature var content = $"{payload}:{options?.KeyId ?? "default"}:{options?.Algorithm ?? SigningAlgorithm.ECDSA_P256_SHA256}"; var hash = SHA256.HashData(Encoding.UTF8.GetBytes(content)); return Convert.ToBase64String(hash); } }