|
|
|
|
@@ -6,12 +6,12 @@ namespace StellaOps.Policy;
|
|
|
|
|
|
|
|
|
|
public static class PolicyEvaluation
|
|
|
|
|
{
|
|
|
|
|
public static PolicyVerdict EvaluateFinding(
|
|
|
|
|
PolicyDocument document,
|
|
|
|
|
PolicyScoringConfig scoringConfig,
|
|
|
|
|
PolicyFinding finding,
|
|
|
|
|
out PolicyExplanation? explanation)
|
|
|
|
|
{
|
|
|
|
|
public static PolicyVerdict EvaluateFinding(
|
|
|
|
|
PolicyDocument document,
|
|
|
|
|
PolicyScoringConfig scoringConfig,
|
|
|
|
|
PolicyFinding finding,
|
|
|
|
|
out PolicyExplanation? explanation)
|
|
|
|
|
{
|
|
|
|
|
if (document is null)
|
|
|
|
|
{
|
|
|
|
|
throw new ArgumentNullException(nameof(document));
|
|
|
|
|
@@ -44,49 +44,49 @@ public static class PolicyEvaluation
|
|
|
|
|
resolvedReachabilityKey);
|
|
|
|
|
var unknownConfidence = ComputeUnknownConfidence(scoringConfig.UnknownConfidence, finding);
|
|
|
|
|
|
|
|
|
|
foreach (var rule in document.Rules)
|
|
|
|
|
{
|
|
|
|
|
if (!RuleMatches(rule, finding))
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return BuildVerdict(rule, finding, scoringConfig, components, unknownConfidence, out explanation);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
explanation = new PolicyExplanation(
|
|
|
|
|
finding.FindingId,
|
|
|
|
|
PolicyVerdictStatus.Allowed,
|
|
|
|
|
null,
|
|
|
|
|
"No rule matched; baseline applied",
|
|
|
|
|
ImmutableArray.Create(PolicyExplanationNode.Leaf("rule", "No matching rule")));
|
|
|
|
|
|
|
|
|
|
var baseline = PolicyVerdict.CreateBaseline(finding.FindingId, scoringConfig);
|
|
|
|
|
return ApplyUnknownConfidence(baseline, unknownConfidence);
|
|
|
|
|
}
|
|
|
|
|
foreach (var rule in document.Rules)
|
|
|
|
|
{
|
|
|
|
|
if (!RuleMatches(rule, finding))
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static PolicyVerdict BuildVerdict(
|
|
|
|
|
PolicyRule rule,
|
|
|
|
|
PolicyFinding finding,
|
|
|
|
|
PolicyScoringConfig config,
|
|
|
|
|
ScoringComponents components,
|
|
|
|
|
UnknownConfidenceResult? unknownConfidence,
|
|
|
|
|
out PolicyExplanation explanation)
|
|
|
|
|
{
|
|
|
|
|
return BuildVerdict(rule, finding, scoringConfig, components, unknownConfidence, out explanation);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
explanation = new PolicyExplanation(
|
|
|
|
|
finding.FindingId,
|
|
|
|
|
PolicyVerdictStatus.Pass,
|
|
|
|
|
null,
|
|
|
|
|
"No rule matched; baseline applied",
|
|
|
|
|
ImmutableArray.Create(PolicyExplanationNode.Leaf("rule", "No matching rule")));
|
|
|
|
|
|
|
|
|
|
var baseline = PolicyVerdict.CreateBaseline(finding.FindingId, scoringConfig);
|
|
|
|
|
return ApplyUnknownConfidence(baseline, unknownConfidence);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static PolicyVerdict BuildVerdict(
|
|
|
|
|
PolicyRule rule,
|
|
|
|
|
PolicyFinding finding,
|
|
|
|
|
PolicyScoringConfig config,
|
|
|
|
|
ScoringComponents components,
|
|
|
|
|
UnknownConfidenceResult? unknownConfidence,
|
|
|
|
|
out PolicyExplanation explanation)
|
|
|
|
|
{
|
|
|
|
|
var action = rule.Action;
|
|
|
|
|
var status = MapAction(action);
|
|
|
|
|
var notes = BuildNotes(action);
|
|
|
|
|
var explanationNodes = ImmutableArray.CreateBuilder<PolicyExplanationNode>();
|
|
|
|
|
explanationNodes.Add(PolicyExplanationNode.Leaf("rule", $"Matched rule '{rule.Name}'", rule.Identifier));
|
|
|
|
|
var notes = BuildNotes(action);
|
|
|
|
|
var explanationNodes = ImmutableArray.CreateBuilder<PolicyExplanationNode>();
|
|
|
|
|
explanationNodes.Add(PolicyExplanationNode.Leaf("rule", $"Matched rule '{rule.Name}'", rule.Identifier));
|
|
|
|
|
var inputs = ImmutableDictionary.CreateBuilder<string, double>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
|
inputs["severityWeight"] = components.SeverityWeight;
|
|
|
|
|
inputs["trustWeight"] = components.TrustWeight;
|
|
|
|
|
inputs["reachabilityWeight"] = components.ReachabilityWeight;
|
|
|
|
|
inputs["baseScore"] = components.BaseScore;
|
|
|
|
|
explanationNodes.Add(PolicyExplanationNode.Branch("score", "Base score", components.BaseScore.ToString(CultureInfo.InvariantCulture),
|
|
|
|
|
PolicyExplanationNode.Leaf("severityWeight", "Severity weight", components.SeverityWeight.ToString(CultureInfo.InvariantCulture)),
|
|
|
|
|
PolicyExplanationNode.Leaf("trustWeight", "Trust weight", components.TrustWeight.ToString(CultureInfo.InvariantCulture)),
|
|
|
|
|
PolicyExplanationNode.Leaf("reachabilityWeight", "Reachability weight", components.ReachabilityWeight.ToString(CultureInfo.InvariantCulture))));
|
|
|
|
|
inputs["baseScore"] = components.BaseScore;
|
|
|
|
|
explanationNodes.Add(PolicyExplanationNode.Branch("score", "Base score", components.BaseScore.ToString(CultureInfo.InvariantCulture),
|
|
|
|
|
PolicyExplanationNode.Leaf("severityWeight", "Severity weight", components.SeverityWeight.ToString(CultureInfo.InvariantCulture)),
|
|
|
|
|
PolicyExplanationNode.Leaf("trustWeight", "Trust weight", components.TrustWeight.ToString(CultureInfo.InvariantCulture)),
|
|
|
|
|
PolicyExplanationNode.Leaf("reachabilityWeight", "Reachability weight", components.ReachabilityWeight.ToString(CultureInfo.InvariantCulture))));
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(components.TrustKey))
|
|
|
|
|
{
|
|
|
|
|
inputs[$"trustWeight.{components.TrustKey}"] = components.TrustWeight;
|
|
|
|
|
@@ -97,14 +97,14 @@ public static class PolicyEvaluation
|
|
|
|
|
}
|
|
|
|
|
if (unknownConfidence is { Band.Description: { Length: > 0 } description })
|
|
|
|
|
{
|
|
|
|
|
notes = AppendNote(notes, description);
|
|
|
|
|
explanationNodes.Add(PolicyExplanationNode.Leaf("unknown", description));
|
|
|
|
|
}
|
|
|
|
|
if (unknownConfidence is { } unknownDetails)
|
|
|
|
|
{
|
|
|
|
|
inputs["unknownConfidence"] = unknownDetails.Confidence;
|
|
|
|
|
inputs["unknownAgeDays"] = unknownDetails.AgeDays;
|
|
|
|
|
}
|
|
|
|
|
notes = AppendNote(notes, description);
|
|
|
|
|
explanationNodes.Add(PolicyExplanationNode.Leaf("unknown", description));
|
|
|
|
|
}
|
|
|
|
|
if (unknownConfidence is { } unknownDetails)
|
|
|
|
|
{
|
|
|
|
|
inputs["unknownConfidence"] = unknownDetails.Confidence;
|
|
|
|
|
inputs["unknownAgeDays"] = unknownDetails.AgeDays;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
double score = components.BaseScore;
|
|
|
|
|
string? quietedBy = null;
|
|
|
|
|
@@ -113,8 +113,8 @@ public static class PolicyEvaluation
|
|
|
|
|
var quietRequested = action.Quiet;
|
|
|
|
|
var quietAllowed = quietRequested && (action.RequireVex is not null || action.Type == PolicyActionType.RequireVex);
|
|
|
|
|
|
|
|
|
|
if (quietRequested && !quietAllowed)
|
|
|
|
|
{
|
|
|
|
|
if (quietRequested && !quietAllowed)
|
|
|
|
|
{
|
|
|
|
|
var warnInputs = ImmutableDictionary.CreateBuilder<string, double>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
|
foreach (var pair in inputs)
|
|
|
|
|
{
|
|
|
|
|
@@ -131,17 +131,17 @@ public static class PolicyEvaluation
|
|
|
|
|
var warnScore = Math.Max(0, components.BaseScore - warnPenalty);
|
|
|
|
|
var warnNotes = AppendNote(notes, "Quiet flag ignored: rule must specify requireVex justifications.");
|
|
|
|
|
|
|
|
|
|
explanation = new PolicyExplanation(
|
|
|
|
|
finding.FindingId,
|
|
|
|
|
PolicyVerdictStatus.Warned,
|
|
|
|
|
rule.Name,
|
|
|
|
|
"Quiet flag ignored; requireVex not provided",
|
|
|
|
|
explanationNodes.ToImmutable());
|
|
|
|
|
|
|
|
|
|
return new PolicyVerdict(
|
|
|
|
|
finding.FindingId,
|
|
|
|
|
PolicyVerdictStatus.Warned,
|
|
|
|
|
rule.Name,
|
|
|
|
|
explanation = new PolicyExplanation(
|
|
|
|
|
finding.FindingId,
|
|
|
|
|
PolicyVerdictStatus.Warned,
|
|
|
|
|
rule.Name,
|
|
|
|
|
"Quiet flag ignored; requireVex not provided",
|
|
|
|
|
explanationNodes.ToImmutable());
|
|
|
|
|
|
|
|
|
|
return new PolicyVerdict(
|
|
|
|
|
finding.FindingId,
|
|
|
|
|
PolicyVerdictStatus.Warned,
|
|
|
|
|
rule.Name,
|
|
|
|
|
action.Type.ToString(),
|
|
|
|
|
warnNotes,
|
|
|
|
|
warnScore,
|
|
|
|
|
@@ -156,56 +156,49 @@ public static class PolicyEvaluation
|
|
|
|
|
Reachability: components.ReachabilityKey);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (status != PolicyVerdictStatus.Allowed)
|
|
|
|
|
{
|
|
|
|
|
explanationNodes.Add(PolicyExplanationNode.Leaf("action", $"Action {action.Type}", status.ToString()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (status)
|
|
|
|
|
{
|
|
|
|
|
case PolicyVerdictStatus.Ignored:
|
|
|
|
|
score = ApplyPenalty(score, config.IgnorePenalty, inputs, "ignorePenalty");
|
|
|
|
|
explanationNodes.Add(PolicyExplanationNode.Leaf("penalty", "Ignore penalty", config.IgnorePenalty.ToString(CultureInfo.InvariantCulture)));
|
|
|
|
|
break;
|
|
|
|
|
case PolicyVerdictStatus.Warned:
|
|
|
|
|
score = ApplyPenalty(score, config.WarnPenalty, inputs, "warnPenalty");
|
|
|
|
|
explanationNodes.Add(PolicyExplanationNode.Leaf("penalty", "Warn penalty", config.WarnPenalty.ToString(CultureInfo.InvariantCulture)));
|
|
|
|
|
break;
|
|
|
|
|
case PolicyVerdictStatus.Deferred:
|
|
|
|
|
var deferPenalty = config.WarnPenalty / 2;
|
|
|
|
|
score = ApplyPenalty(score, deferPenalty, inputs, "deferPenalty");
|
|
|
|
|
explanationNodes.Add(PolicyExplanationNode.Leaf("penalty", "Defer penalty", deferPenalty.ToString(CultureInfo.InvariantCulture)));
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if (status != PolicyVerdictStatus.Pass)
|
|
|
|
|
{
|
|
|
|
|
explanationNodes.Add(PolicyExplanationNode.Leaf("action", $"Action {action.Type}", status.ToString()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (quietAllowed)
|
|
|
|
|
{
|
|
|
|
|
score = ApplyPenalty(score, config.QuietPenalty, inputs, "quietPenalty");
|
|
|
|
|
quietedBy = rule.Name;
|
|
|
|
|
quiet = true;
|
|
|
|
|
explanationNodes.Add(PolicyExplanationNode.Leaf("quiet", "Quiet applied", config.QuietPenalty.ToString(CultureInfo.InvariantCulture)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
explanation = new PolicyExplanation(
|
|
|
|
|
finding.FindingId,
|
|
|
|
|
status,
|
|
|
|
|
rule.Name,
|
|
|
|
|
notes,
|
|
|
|
|
explanationNodes.ToImmutable());
|
|
|
|
|
|
|
|
|
|
explanation = new PolicyExplanation(
|
|
|
|
|
finding.FindingId,
|
|
|
|
|
status,
|
|
|
|
|
rule.Name,
|
|
|
|
|
notes,
|
|
|
|
|
explanationNodes.ToImmutable());
|
|
|
|
|
|
|
|
|
|
return new PolicyVerdict(
|
|
|
|
|
finding.FindingId,
|
|
|
|
|
status,
|
|
|
|
|
rule.Name,
|
|
|
|
|
action.Type.ToString(),
|
|
|
|
|
notes,
|
|
|
|
|
switch (status)
|
|
|
|
|
{
|
|
|
|
|
case PolicyVerdictStatus.Ignored:
|
|
|
|
|
score = ApplyPenalty(score, config.IgnorePenalty, inputs, "ignorePenalty");
|
|
|
|
|
explanationNodes.Add(PolicyExplanationNode.Leaf("penalty", "Ignore penalty", config.IgnorePenalty.ToString(CultureInfo.InvariantCulture)));
|
|
|
|
|
break;
|
|
|
|
|
case PolicyVerdictStatus.Warned:
|
|
|
|
|
score = ApplyPenalty(score, config.WarnPenalty, inputs, "warnPenalty");
|
|
|
|
|
explanationNodes.Add(PolicyExplanationNode.Leaf("penalty", "Warn penalty", config.WarnPenalty.ToString(CultureInfo.InvariantCulture)));
|
|
|
|
|
break;
|
|
|
|
|
case PolicyVerdictStatus.Deferred:
|
|
|
|
|
var deferPenalty = config.WarnPenalty / 2;
|
|
|
|
|
score = ApplyPenalty(score, deferPenalty, inputs, "deferPenalty");
|
|
|
|
|
explanationNodes.Add(PolicyExplanationNode.Leaf("penalty", "Defer penalty", deferPenalty.ToString(CultureInfo.InvariantCulture)));
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (quietAllowed)
|
|
|
|
|
{
|
|
|
|
|
score = ApplyPenalty(score, config.QuietPenalty, inputs, "quietPenalty");
|
|
|
|
|
quietedBy = rule.Name;
|
|
|
|
|
quiet = true;
|
|
|
|
|
explanationNodes.Add(PolicyExplanationNode.Leaf("quiet", "Quiet applied", config.QuietPenalty.ToString(CultureInfo.InvariantCulture)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
explanation = new PolicyExplanation(
|
|
|
|
|
finding.FindingId,
|
|
|
|
|
status,
|
|
|
|
|
rule.Name,
|
|
|
|
|
notes ?? string.Empty,
|
|
|
|
|
explanationNodes.ToImmutable());
|
|
|
|
|
|
|
|
|
|
return new PolicyVerdict(
|
|
|
|
|
finding.FindingId,
|
|
|
|
|
status,
|
|
|
|
|
rule.Name,
|
|
|
|
|
action.Type.ToString(),
|
|
|
|
|
notes,
|
|
|
|
|
score,
|
|
|
|
|
config.Version,
|
|
|
|
|
inputs.ToImmutable(),
|
|
|
|
|
@@ -229,12 +222,12 @@ public static class PolicyEvaluation
|
|
|
|
|
return Math.Max(0, score - penalty);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static PolicyVerdict ApplyUnknownConfidence(PolicyVerdict verdict, UnknownConfidenceResult? unknownConfidence)
|
|
|
|
|
{
|
|
|
|
|
if (unknownConfidence is null)
|
|
|
|
|
{
|
|
|
|
|
return verdict;
|
|
|
|
|
}
|
|
|
|
|
private static PolicyVerdict ApplyUnknownConfidence(PolicyVerdict verdict, UnknownConfidenceResult? unknownConfidence)
|
|
|
|
|
{
|
|
|
|
|
if (unknownConfidence is null)
|
|
|
|
|
{
|
|
|
|
|
return verdict;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var inputsBuilder = ImmutableDictionary.CreateBuilder<string, double>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
|
foreach (var pair in verdict.GetInputs())
|
|
|
|
|
@@ -245,12 +238,12 @@ public static class PolicyEvaluation
|
|
|
|
|
inputsBuilder["unknownConfidence"] = unknownConfidence.Value.Confidence;
|
|
|
|
|
inputsBuilder["unknownAgeDays"] = unknownConfidence.Value.AgeDays;
|
|
|
|
|
|
|
|
|
|
return verdict with
|
|
|
|
|
{
|
|
|
|
|
Inputs = inputsBuilder.ToImmutable(),
|
|
|
|
|
UnknownConfidence = unknownConfidence.Value.Confidence,
|
|
|
|
|
ConfidenceBand = unknownConfidence.Value.Band.Name,
|
|
|
|
|
UnknownAgeDays = unknownConfidence.Value.AgeDays,
|
|
|
|
|
return verdict with
|
|
|
|
|
{
|
|
|
|
|
Inputs = inputsBuilder.ToImmutable(),
|
|
|
|
|
UnknownConfidence = unknownConfidence.Value.Confidence,
|
|
|
|
|
ConfidenceBand = unknownConfidence.Value.Band.Name,
|
|
|
|
|
UnknownAgeDays = unknownConfidence.Value.AgeDays,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|