Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
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
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
143 lines
5.6 KiB
C#
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, out _);
|
|
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();
|
|
}
|
|
}
|