using System.Collections.Immutable; using System.Security.Cryptography; using System.Text; using StellaOps.Policy.Engine.Domain; namespace StellaOps.Policy.Engine.Services; /// /// Compiles policy DSL to canonical representation, signs it deterministically, and stores per revision. /// Captures AOC (Attestation of Compliance) metadata for policy revisions. /// internal sealed class PolicyBundleService { private readonly PolicyCompilationService _compilationService; private readonly IPolicyPackRepository _repository; private readonly TimeProvider _timeProvider; public PolicyBundleService( PolicyCompilationService compilationService, IPolicyPackRepository repository, TimeProvider timeProvider) { _compilationService = compilationService ?? throw new ArgumentNullException(nameof(compilationService)); _repository = repository ?? throw new ArgumentNullException(nameof(repository)); _timeProvider = timeProvider ?? TimeProvider.System; } public async Task CompileAndStoreAsync( string packId, int version, PolicyBundleRequest request, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(packId)) { throw new ArgumentException("packId is required", nameof(packId)); } if (request is null) { throw new ArgumentNullException(nameof(request)); } var compiledAt = _timeProvider.GetUtcNow(); var compileResult = _compilationService.Compile(new PolicyCompileRequest(request.Dsl)); if (!compileResult.Success || compileResult.CanonicalRepresentation.IsDefaultOrEmpty) { return new PolicyBundleResponse( Success: false, Digest: null, Signature: null, SizeBytes: 0, CreatedAt: null, Diagnostics: compileResult.Diagnostics, AocMetadata: null); } var payload = compileResult.CanonicalRepresentation.ToArray(); var artifactDigest = compileResult.Digest ?? $"sha256:{ComputeSha256Hex(payload)}"; var sourceDigest = ComputeSourceDigest(request.Dsl.Source); var signature = Sign(artifactDigest, request.SigningKeyId); var createdAt = _timeProvider.GetUtcNow(); // Generate AOC metadata var compilationId = GenerateCompilationId(packId, version, compiledAt); var aocMetadata = CreateAocMetadata( compilationId, request.Dsl.Syntax, compiledAt, sourceDigest, artifactDigest, compileResult, request.Provenance); var record = new PolicyBundleRecord( Digest: artifactDigest, Signature: signature, Size: payload.Length, CreatedAt: createdAt, Payload: payload.ToImmutableArray(), AocMetadata: aocMetadata); await _repository.StoreBundleAsync(packId, version, record, cancellationToken).ConfigureAwait(false); var aocResponse = new PolicyAocMetadataResponse( CompilationId: aocMetadata.CompilationId, CompilerVersion: aocMetadata.CompilerVersion, CompiledAt: aocMetadata.CompiledAt, SourceDigest: aocMetadata.SourceDigest, ArtifactDigest: aocMetadata.ArtifactDigest, ComplexityScore: aocMetadata.ComplexityScore, RuleCount: aocMetadata.RuleCount, DurationMilliseconds: aocMetadata.DurationMilliseconds); return new PolicyBundleResponse( Success: true, Digest: artifactDigest, Signature: signature, SizeBytes: payload.Length, CreatedAt: createdAt, Diagnostics: compileResult.Diagnostics, AocMetadata: aocResponse); } private static string ComputeSha256Hex(byte[] payload) { Span hash = stackalloc byte[32]; SHA256.HashData(payload, hash); return Convert.ToHexString(hash).ToLowerInvariant(); } private static string ComputeSourceDigest(string source) { var bytes = Encoding.UTF8.GetBytes(source); Span hash = stackalloc byte[32]; SHA256.HashData(bytes, hash); return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}"; } private static string Sign(string digest, string? signingKeyId) { // Deterministic signature stub suitable for offline testing. var key = string.IsNullOrWhiteSpace(signingKeyId) ? "policy-dev-signer" : signingKeyId.Trim(); var mac = HMACSHA256.HashData(Encoding.UTF8.GetBytes(key), Encoding.UTF8.GetBytes(digest)); return $"sig:sha256:{Convert.ToHexString(mac).ToLowerInvariant()}"; } private static string GenerateCompilationId(string packId, int version, DateTimeOffset timestamp) { // Deterministic compilation ID based on pack, version, and timestamp var input = $"{packId}:{version}:{timestamp:O}"; var bytes = Encoding.UTF8.GetBytes(input); Span hash = stackalloc byte[32]; SHA256.HashData(bytes, hash); return $"comp-{Convert.ToHexString(hash).ToLowerInvariant()[..16]}"; } private static PolicyAocMetadata CreateAocMetadata( string compilationId, string compilerVersion, DateTimeOffset compiledAt, string sourceDigest, string artifactDigest, PolicyCompilationResultDto compileResult, PolicyProvenanceInput? provenanceInput) { var complexity = compileResult.Complexity; var statistics = compileResult.Statistics; PolicyProvenance? provenance = null; if (provenanceInput is not null) { provenance = new PolicyProvenance( SourceType: provenanceInput.SourceType, SourceUrl: provenanceInput.SourceUrl, Submitter: provenanceInput.Submitter, CommitSha: provenanceInput.CommitSha, Branch: provenanceInput.Branch, IngestedAt: compiledAt); } return new PolicyAocMetadata( CompilationId: compilationId, CompilerVersion: compilerVersion, CompiledAt: compiledAt, SourceDigest: sourceDigest, ArtifactDigest: artifactDigest, ComplexityScore: complexity?.Score ?? 0, RuleCount: statistics?.RuleCount ?? complexity?.RuleCount ?? 0, DurationMilliseconds: compileResult.DurationMilliseconds, Provenance: provenance, AttestationRef: null); } }