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:
270
src/StellaOps.Policy/PolicyEvaluation.cs
Normal file
270
src/StellaOps.Policy/PolicyEvaluation.cs
Normal file
@@ -0,0 +1,270 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user