Files
git.stella-ops.org/src/StellaOps.Policy/PolicyEvaluation.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

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