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);
}
}