Files
git.stella-ops.org/src/Policy/StellaOps.Policy.Registry/Services/PublishPipelineService.cs
StellaOps Bot 0de92144d2
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
mock-dev-release / package-mock-release (push) Has been cancelled
feat(api): Implement Console Export Client and Models
- Added ConsoleExportClient for managing export requests and responses.
- Introduced ConsoleExportRequest and ConsoleExportResponse models.
- Implemented methods for creating and retrieving exports with appropriate headers.

feat(crypto): Add Software SM2/SM3 Cryptography Provider

- Implemented SmSoftCryptoProvider for software-only SM2/SM3 cryptography.
- Added support for signing and verification using SM2 algorithm.
- Included hashing functionality with SM3 algorithm.
- Configured options for loading keys from files and environment gate checks.

test(crypto): Add unit tests for SmSoftCryptoProvider

- Created comprehensive tests for signing, verifying, and hashing functionalities.
- Ensured correct behavior for key management and error handling.

feat(api): Enhance Console Export Models

- Expanded ConsoleExport models to include detailed status and event types.
- Added support for various export formats and notification options.

test(time): Implement TimeAnchorPolicyService tests

- Developed tests for TimeAnchorPolicyService to validate time anchors.
- Covered scenarios for anchor validation, drift calculation, and policy enforcement.
2025-12-07 00:27:33 +02:00

444 lines
15 KiB
C#

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;
/// <summary>
/// Default implementation of publish pipeline service.
/// Handles policy pack publication with attestation generation.
/// </summary>
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<PublishResult> 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<PublicationStatus?> GetPublicationStatusAsync(
Guid tenantId,
Guid packId,
CancellationToken cancellationToken = default)
{
_publications.TryGetValue((tenantId, packId), out var status);
return Task.FromResult(status);
}
public Task<PolicyPackAttestation?> GetAttestationAsync(
Guid tenantId,
Guid packId,
CancellationToken cancellationToken = default)
{
_attestations.TryGetValue((tenantId, packId), out var attestation);
return Task.FromResult(attestation);
}
public async Task<AttestationVerificationResult> VerifyAttestationAsync(
Guid tenantId,
Guid packId,
CancellationToken cancellationToken = default)
{
var checks = new List<VerificationCheck>();
var errors = new List<string>();
var warnings = new List<string>();
// 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<PublishedPackList> 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<RevokeResult> 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<string, string>
{
["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);
}
}