up
	
		
			
	
		
	
	
		
	
		
			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
				
			
		
		
	
	
				
					
				
			
		
			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
				
			This commit is contained in:
		
							
								
								
									
										142
									
								
								src/StellaOps.Policy/PolicyPreviewService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								src/StellaOps.Policy/PolicyPreviewService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,142 @@ | ||||
| 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(); | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user