Files
git.stella-ops.org/src/StellaOps.Policy/PolicyPreviewService.cs
master daa6a4ae8c
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Build Test Deploy / build-test (push) Has been cancelled
Build Test Deploy / authority-container (push) Has been cancelled
Build Test Deploy / docs (push) Has been cancelled
Build Test Deploy / deploy (push) Has been cancelled
up
2025-10-19 10:38:55 +03:00

143 lines
5.6 KiB
C#

using System;
using System.Collections.Generic;
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace StellaOps.Policy;
public sealed class PolicyPreviewService
{
private readonly PolicySnapshotStore _snapshotStore;
private readonly ILogger<PolicyPreviewService> _logger;
public PolicyPreviewService(PolicySnapshotStore snapshotStore, ILogger<PolicyPreviewService> logger)
{
_snapshotStore = snapshotStore ?? throw new ArgumentNullException(nameof(snapshotStore));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<PolicyPreviewResponse> PreviewAsync(PolicyPreviewRequest request, CancellationToken cancellationToken = default)
{
if (request is null)
{
throw new ArgumentNullException(nameof(request));
}
var (snapshot, bindingIssues) = await ResolveSnapshotAsync(request, cancellationToken).ConfigureAwait(false);
if (snapshot is null)
{
_logger.LogWarning("Policy preview failed: snapshot unavailable or validation errors. Issues={Count}", bindingIssues.Length);
return new PolicyPreviewResponse(false, string.Empty, null, bindingIssues, ImmutableArray<PolicyVerdictDiff>.Empty, 0);
}
var projected = Evaluate(snapshot.Document, snapshot.ScoringConfig, request.Findings);
var baseline = BuildBaseline(request.BaselineVerdicts, projected, snapshot.ScoringConfig);
var diffs = BuildDiffs(baseline, projected);
var changed = diffs.Count(static diff => diff.Changed);
_logger.LogDebug("Policy preview computed for {ImageDigest}. Changed={Changed}", request.ImageDigest, changed);
return new PolicyPreviewResponse(true, snapshot.Digest, snapshot.RevisionId, bindingIssues, diffs, changed);
}
private async Task<(PolicySnapshot? Snapshot, ImmutableArray<PolicyIssue> Issues)> ResolveSnapshotAsync(PolicyPreviewRequest request, CancellationToken cancellationToken)
{
if (request.ProposedPolicy is not null)
{
var binding = PolicyBinder.Bind(request.ProposedPolicy.Content, request.ProposedPolicy.Format);
if (!binding.Success)
{
return (null, binding.Issues);
}
var digest = PolicyDigest.Compute(binding.Document);
var snapshot = new PolicySnapshot(
request.SnapshotOverride?.RevisionNumber + 1 ?? 0,
request.SnapshotOverride?.RevisionId ?? "preview",
digest,
DateTimeOffset.UtcNow,
request.ProposedPolicy.Actor,
request.ProposedPolicy.Format,
binding.Document,
binding.Issues,
PolicyScoringConfig.Default);
return (snapshot, binding.Issues);
}
if (request.SnapshotOverride is not null)
{
return (request.SnapshotOverride, ImmutableArray<PolicyIssue>.Empty);
}
var latest = await _snapshotStore.GetLatestAsync(cancellationToken).ConfigureAwait(false);
if (latest is not null)
{
return (latest, ImmutableArray<PolicyIssue>.Empty);
}
return (null, ImmutableArray.Create(PolicyIssue.Error("policy.preview.snapshot_missing", "No policy snapshot is available for preview.", "$")));
}
private static ImmutableArray<PolicyVerdict> Evaluate(PolicyDocument document, PolicyScoringConfig scoringConfig, ImmutableArray<PolicyFinding> findings)
{
if (findings.IsDefaultOrEmpty)
{
return ImmutableArray<PolicyVerdict>.Empty;
}
var results = ImmutableArray.CreateBuilder<PolicyVerdict>(findings.Length);
foreach (var finding in findings)
{
var verdict = PolicyEvaluation.EvaluateFinding(document, scoringConfig, finding);
results.Add(verdict);
}
return results.ToImmutable();
}
private static ImmutableDictionary<string, PolicyVerdict> BuildBaseline(ImmutableArray<PolicyVerdict> baseline, ImmutableArray<PolicyVerdict> projected, PolicyScoringConfig scoringConfig)
{
var builder = ImmutableDictionary.CreateBuilder<string, PolicyVerdict>(StringComparer.Ordinal);
if (!baseline.IsDefaultOrEmpty)
{
foreach (var verdict in baseline)
{
if (!string.IsNullOrEmpty(verdict.FindingId) && !builder.ContainsKey(verdict.FindingId))
{
builder.Add(verdict.FindingId, verdict);
}
}
}
foreach (var verdict in projected)
{
if (!builder.ContainsKey(verdict.FindingId))
{
builder.Add(verdict.FindingId, PolicyVerdict.CreateBaseline(verdict.FindingId, scoringConfig));
}
}
return builder.ToImmutable();
}
private static ImmutableArray<PolicyVerdictDiff> BuildDiffs(ImmutableDictionary<string, PolicyVerdict> baseline, ImmutableArray<PolicyVerdict> projected)
{
var diffs = ImmutableArray.CreateBuilder<PolicyVerdictDiff>(projected.Length);
foreach (var verdict in projected.OrderBy(static v => v.FindingId, StringComparer.Ordinal))
{
var baseVerdict = baseline.TryGetValue(verdict.FindingId, out var existing)
? existing
: new PolicyVerdict(verdict.FindingId, PolicyVerdictStatus.Pass);
diffs.Add(new PolicyVerdictDiff(baseVerdict, verdict));
}
return diffs.ToImmutable();
}
}