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
				
			
		
			
				
	
	
		
			271 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			271 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
| using System;
 | |
| using System.Collections.Immutable;
 | |
| 
 | |
| namespace StellaOps.Policy;
 | |
| 
 | |
| public static class PolicyEvaluation
 | |
| {
 | |
|     public static PolicyVerdict EvaluateFinding(PolicyDocument document, PolicyScoringConfig scoringConfig, PolicyFinding finding)
 | |
|     {
 | |
|         if (document is null)
 | |
|         {
 | |
|             throw new ArgumentNullException(nameof(document));
 | |
|         }
 | |
| 
 | |
|         if (scoringConfig is null)
 | |
|         {
 | |
|             throw new ArgumentNullException(nameof(scoringConfig));
 | |
|         }
 | |
| 
 | |
|         if (finding is null)
 | |
|         {
 | |
|             throw new ArgumentNullException(nameof(finding));
 | |
|         }
 | |
| 
 | |
|         var severityWeight = scoringConfig.SeverityWeights.TryGetValue(finding.Severity, out var weight)
 | |
|             ? weight
 | |
|             : scoringConfig.SeverityWeights.GetValueOrDefault(PolicySeverity.Unknown, 0);
 | |
| 
 | |
|         foreach (var rule in document.Rules)
 | |
|         {
 | |
|             if (!RuleMatches(rule, finding))
 | |
|             {
 | |
|                 continue;
 | |
|             }
 | |
| 
 | |
|             return BuildVerdict(rule, finding, scoringConfig, severityWeight);
 | |
|         }
 | |
| 
 | |
|         return PolicyVerdict.CreateBaseline(finding.FindingId, scoringConfig);
 | |
|     }
 | |
| 
 | |
|     private static PolicyVerdict BuildVerdict(
 | |
|         PolicyRule rule,
 | |
|         PolicyFinding finding,
 | |
|         PolicyScoringConfig config,
 | |
|         double severityWeight)
 | |
|     {
 | |
|         var action = rule.Action;
 | |
|         var status = MapAction(action);
 | |
|         var notes = BuildNotes(action);
 | |
|         var inputs = ImmutableDictionary.CreateBuilder<string, double>(StringComparer.OrdinalIgnoreCase);
 | |
|         inputs["severityWeight"] = severityWeight;
 | |
| 
 | |
|         double score = severityWeight;
 | |
|         string? quietedBy = null;
 | |
|         var quiet = false;
 | |
| 
 | |
|         switch (status)
 | |
|         {
 | |
|             case PolicyVerdictStatus.Ignored:
 | |
|                 score = Math.Max(0, severityWeight - config.IgnorePenalty);
 | |
|                 inputs["ignorePenalty"] = config.IgnorePenalty;
 | |
|                 break;
 | |
|             case PolicyVerdictStatus.Warned:
 | |
|                 score = Math.Max(0, severityWeight - config.WarnPenalty);
 | |
|                 inputs["warnPenalty"] = config.WarnPenalty;
 | |
|                 break;
 | |
|             case PolicyVerdictStatus.Deferred:
 | |
|                 score = Math.Max(0, severityWeight - (config.WarnPenalty / 2));
 | |
|                 inputs["deferPenalty"] = config.WarnPenalty / 2;
 | |
|                 break;
 | |
|         }
 | |
| 
 | |
|         if (action.Quiet)
 | |
|         {
 | |
|             var quietAllowed = action.RequireVex is not null || action.Type == PolicyActionType.RequireVex;
 | |
|             if (quietAllowed)
 | |
|             {
 | |
|                 score = Math.Max(0, score - config.QuietPenalty);
 | |
|                 inputs["quietPenalty"] = config.QuietPenalty;
 | |
|                 quietedBy = rule.Name;
 | |
|                 quiet = true;
 | |
|             }
 | |
|             else
 | |
|             {
 | |
|                 inputs.Remove("ignorePenalty");
 | |
|                 var warnScore = Math.Max(0, severityWeight - config.WarnPenalty);
 | |
|                 inputs["warnPenalty"] = config.WarnPenalty;
 | |
|                 var warnNotes = AppendNote(notes, "Quiet flag ignored: rule must specify requireVex justifications.");
 | |
| 
 | |
|                 return new PolicyVerdict(
 | |
|                     finding.FindingId,
 | |
|                     PolicyVerdictStatus.Warned,
 | |
|                     rule.Name,
 | |
|                     action.Type.ToString(),
 | |
|                     warnNotes,
 | |
|                     warnScore,
 | |
|                     config.Version,
 | |
|                     inputs.ToImmutable(),
 | |
|                     QuietedBy: null,
 | |
|                     Quiet: false);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         return new PolicyVerdict(
 | |
|             finding.FindingId,
 | |
|             status,
 | |
|             rule.Name,
 | |
|             action.Type.ToString(),
 | |
|             notes,
 | |
|             score,
 | |
|             config.Version,
 | |
|             inputs.ToImmutable(),
 | |
|             quietedBy,
 | |
|             quiet);
 | |
|     }
 | |
| 
 | |
|     private static bool RuleMatches(PolicyRule rule, PolicyFinding finding)
 | |
|     {
 | |
|         if (!rule.Severities.IsDefaultOrEmpty && !rule.Severities.Contains(finding.Severity))
 | |
|         {
 | |
|             return false;
 | |
|         }
 | |
| 
 | |
|         if (!Matches(rule.Environments, finding.Environment))
 | |
|         {
 | |
|             return false;
 | |
|         }
 | |
| 
 | |
|         if (!Matches(rule.Sources, finding.Source))
 | |
|         {
 | |
|             return false;
 | |
|         }
 | |
| 
 | |
|         if (!Matches(rule.Vendors, finding.Vendor))
 | |
|         {
 | |
|             return false;
 | |
|         }
 | |
| 
 | |
|         if (!Matches(rule.Licenses, finding.License))
 | |
|         {
 | |
|             return false;
 | |
|         }
 | |
| 
 | |
|         if (!RuleMatchCriteria(rule.Match, finding))
 | |
|         {
 | |
|             return false;
 | |
|         }
 | |
| 
 | |
|         return true;
 | |
|     }
 | |
| 
 | |
|     private static bool Matches(ImmutableArray<string> ruleValues, string? candidate)
 | |
|     {
 | |
|         if (ruleValues.IsDefaultOrEmpty)
 | |
|         {
 | |
|             return true;
 | |
|         }
 | |
| 
 | |
|         if (string.IsNullOrWhiteSpace(candidate))
 | |
|         {
 | |
|             return false;
 | |
|         }
 | |
| 
 | |
|         return ruleValues.Contains(candidate, StringComparer.OrdinalIgnoreCase);
 | |
|     }
 | |
| 
 | |
|     private static bool RuleMatchCriteria(PolicyRuleMatchCriteria criteria, PolicyFinding finding)
 | |
|     {
 | |
|         if (!criteria.Images.IsDefaultOrEmpty && !ContainsValue(criteria.Images, finding.Image, StringComparer.OrdinalIgnoreCase))
 | |
|         {
 | |
|             return false;
 | |
|         }
 | |
| 
 | |
|         if (!criteria.Repositories.IsDefaultOrEmpty && !ContainsValue(criteria.Repositories, finding.Repository, StringComparer.OrdinalIgnoreCase))
 | |
|         {
 | |
|             return false;
 | |
|         }
 | |
| 
 | |
|         if (!criteria.Packages.IsDefaultOrEmpty && !ContainsValue(criteria.Packages, finding.Package, StringComparer.OrdinalIgnoreCase))
 | |
|         {
 | |
|             return false;
 | |
|         }
 | |
| 
 | |
|         if (!criteria.Purls.IsDefaultOrEmpty && !ContainsValue(criteria.Purls, finding.Purl, StringComparer.OrdinalIgnoreCase))
 | |
|         {
 | |
|             return false;
 | |
|         }
 | |
| 
 | |
|         if (!criteria.Cves.IsDefaultOrEmpty && !ContainsValue(criteria.Cves, finding.Cve, StringComparer.OrdinalIgnoreCase))
 | |
|         {
 | |
|             return false;
 | |
|         }
 | |
| 
 | |
|         if (!criteria.Paths.IsDefaultOrEmpty && !ContainsValue(criteria.Paths, finding.Path, StringComparer.Ordinal))
 | |
|         {
 | |
|             return false;
 | |
|         }
 | |
| 
 | |
|         if (!criteria.LayerDigests.IsDefaultOrEmpty && !ContainsValue(criteria.LayerDigests, finding.LayerDigest, StringComparer.OrdinalIgnoreCase))
 | |
|         {
 | |
|             return false;
 | |
|         }
 | |
| 
 | |
|         if (!criteria.UsedByEntrypoint.IsDefaultOrEmpty)
 | |
|         {
 | |
|             var match = false;
 | |
|             foreach (var tag in criteria.UsedByEntrypoint)
 | |
|             {
 | |
|                 if (finding.Tags.Contains(tag, StringComparer.OrdinalIgnoreCase))
 | |
|                 {
 | |
|                     match = true;
 | |
|                     break;
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             if (!match)
 | |
|             {
 | |
|                 return false;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         return true;
 | |
|     }
 | |
| 
 | |
|     private static bool ContainsValue(ImmutableArray<string> values, string? candidate, StringComparer comparer)
 | |
|     {
 | |
|         if (values.IsDefaultOrEmpty)
 | |
|         {
 | |
|             return true;
 | |
|         }
 | |
| 
 | |
|         if (string.IsNullOrWhiteSpace(candidate))
 | |
|         {
 | |
|             return false;
 | |
|         }
 | |
| 
 | |
|         return values.Contains(candidate, comparer);
 | |
|     }
 | |
| 
 | |
|     private static PolicyVerdictStatus MapAction(PolicyAction action)
 | |
|         => action.Type switch
 | |
|         {
 | |
|             PolicyActionType.Block => PolicyVerdictStatus.Blocked,
 | |
|             PolicyActionType.Ignore => PolicyVerdictStatus.Ignored,
 | |
|             PolicyActionType.Warn => PolicyVerdictStatus.Warned,
 | |
|             PolicyActionType.Defer => PolicyVerdictStatus.Deferred,
 | |
|             PolicyActionType.Escalate => PolicyVerdictStatus.Escalated,
 | |
|             PolicyActionType.RequireVex => PolicyVerdictStatus.RequiresVex,
 | |
|             _ => PolicyVerdictStatus.Pass,
 | |
|         };
 | |
| 
 | |
|     private static string? BuildNotes(PolicyAction action)
 | |
|     {
 | |
|         if (action.Ignore is { } ignore && !string.IsNullOrWhiteSpace(ignore.Justification))
 | |
|         {
 | |
|             return ignore.Justification;
 | |
|         }
 | |
| 
 | |
|         if (action.Escalate is { } escalate && escalate.MinimumSeverity is { } severity)
 | |
|         {
 | |
|             return $"Escalate >= {severity}";
 | |
|         }
 | |
| 
 | |
|         return null;
 | |
|     }
 | |
| 
 | |
|     private static string? AppendNote(string? existing, string addition)
 | |
|         => string.IsNullOrWhiteSpace(existing) ? addition : string.Concat(existing, " | ", addition);
 | |
| }
 |