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
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
176 lines
6.6 KiB
C#
176 lines
6.6 KiB
C#
using System.Collections.Immutable;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using StellaOps.Policy.Engine.Domain;
|
|
|
|
namespace StellaOps.Policy.Engine.Services;
|
|
|
|
/// <summary>
|
|
/// Compiles policy DSL to canonical representation, signs it deterministically, and stores per revision.
|
|
/// Captures AOC (Attestation of Compliance) metadata for policy revisions.
|
|
/// </summary>
|
|
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<PolicyBundleResponse> 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<byte> 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<byte> 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<byte> 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);
|
|
}
|
|
}
|