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