Restructure solution layout by module
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using StellaOps.Policy;
|
||||
using StellaOps.Policy.Engine.Compilation;
|
||||
using StellaOps.Policy.Engine.Evaluation;
|
||||
using StellaOps.Policy.Engine.Services;
|
||||
using Xunit;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Tests;
|
||||
|
||||
public sealed class PolicyEvaluatorTests
|
||||
{
|
||||
private static readonly string BaselinePolicy = """
|
||||
policy "Baseline Production Policy" syntax "stella-dsl@1" {
|
||||
metadata {
|
||||
description = "Block critical, escalate high, enforce VEX justifications."
|
||||
tags = ["baseline","production"]
|
||||
}
|
||||
|
||||
profile severity {
|
||||
map vendor_weight {
|
||||
source "GHSA" => +0.5
|
||||
source "OSV" => +0.0
|
||||
}
|
||||
env exposure_adjustments {
|
||||
if env.exposure == "internet" then +0.5
|
||||
}
|
||||
}
|
||||
|
||||
rule block_critical priority 5 {
|
||||
when severity.normalized >= "Critical"
|
||||
then status := "blocked"
|
||||
because "Critical severity must be remediated before deploy."
|
||||
}
|
||||
|
||||
rule escalate_high_internet {
|
||||
when severity.normalized == "High"
|
||||
and env.exposure == "internet"
|
||||
then escalate to severity_band("Critical")
|
||||
because "High severity on internet-exposed asset escalates to critical."
|
||||
}
|
||||
|
||||
rule require_vex_justification {
|
||||
when vex.any(status in ["not_affected","fixed"])
|
||||
and vex.justification in ["component_not_present","vulnerable_code_not_present"]
|
||||
then status := vex.status
|
||||
annotate winning_statement := vex.latest().statementId
|
||||
because "Respect strong vendor VEX claims."
|
||||
}
|
||||
|
||||
rule alert_warn_eol_runtime priority 1 {
|
||||
when severity.normalized <= "Medium"
|
||||
and sbom.has_tag("runtime:eol")
|
||||
then warn message "Runtime marked as EOL; upgrade recommended."
|
||||
because "Deprecated runtime should be upgraded."
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
private readonly PolicyCompiler compiler = new();
|
||||
private readonly PolicyEvaluationService evaluationService = new();
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_BlockCriticalRuleMatches()
|
||||
{
|
||||
var document = CompileBaseline();
|
||||
var context = CreateContext(severity: "Critical", exposure: "internal");
|
||||
|
||||
var result = evaluationService.Evaluate(document, context);
|
||||
|
||||
Assert.True(result.Matched);
|
||||
Assert.Equal("block_critical", result.RuleName);
|
||||
Assert.Equal("blocked", result.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_EscalateAdjustsSeverity()
|
||||
{
|
||||
var document = CompileBaseline();
|
||||
var context = CreateContext(severity: "High", exposure: "internet");
|
||||
|
||||
var result = evaluationService.Evaluate(document, context);
|
||||
|
||||
Assert.True(result.Matched);
|
||||
Assert.Equal("escalate_high_internet", result.RuleName);
|
||||
Assert.Equal("affected", result.Status);
|
||||
Assert.Equal("Critical", result.Severity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_VexOverrideSetsStatusAndAnnotation()
|
||||
{
|
||||
var document = CompileBaseline();
|
||||
var statements = ImmutableArray.Create(
|
||||
new PolicyEvaluationVexStatement("not_affected", "component_not_present", "stmt-001"));
|
||||
var context = CreateContext("Medium", "internal") with
|
||||
{
|
||||
Vex = new PolicyEvaluationVexEvidence(statements)
|
||||
};
|
||||
|
||||
var result = evaluationService.Evaluate(document, context);
|
||||
|
||||
Assert.True(result.Matched);
|
||||
Assert.Equal("require_vex_justification", result.RuleName);
|
||||
Assert.Equal("not_affected", result.Status);
|
||||
Assert.Equal("stmt-001", result.Annotations["winning_statement"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_WarnRuleEmitsWarning()
|
||||
{
|
||||
var document = CompileBaseline();
|
||||
var tags = ImmutableHashSet.Create("runtime:eol");
|
||||
var context = CreateContext("Medium", "internal") with
|
||||
{
|
||||
Sbom = new PolicyEvaluationSbom(tags)
|
||||
};
|
||||
|
||||
var result = evaluationService.Evaluate(document, context);
|
||||
|
||||
Assert.True(result.Matched);
|
||||
Assert.Equal("alert_warn_eol_runtime", result.RuleName);
|
||||
Assert.Equal("warned", result.Status);
|
||||
Assert.Contains(result.Warnings, message => message.Contains("EOL", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_ExceptionSuppressesCriticalFinding()
|
||||
{
|
||||
var document = CompileBaseline();
|
||||
var effect = new PolicyExceptionEffect(
|
||||
Id: "suppress-critical",
|
||||
Name: "Critical Break Glass",
|
||||
Effect: PolicyExceptionEffectType.Suppress,
|
||||
DowngradeSeverity: null,
|
||||
RequiredControlId: null,
|
||||
RoutingTemplate: "secops",
|
||||
MaxDurationDays: 7,
|
||||
Description: null);
|
||||
var scope = PolicyEvaluationExceptionScope.Create(ruleNames: new[] { "block_critical" });
|
||||
var instance = new PolicyEvaluationExceptionInstance(
|
||||
Id: "exc-001",
|
||||
EffectId: effect.Id,
|
||||
Scope: scope,
|
||||
CreatedAt: new DateTimeOffset(2025, 10, 1, 0, 0, 0, TimeSpan.Zero),
|
||||
Metadata: ImmutableDictionary<string, string>.Empty);
|
||||
var exceptions = new PolicyEvaluationExceptions(
|
||||
ImmutableDictionary<string, PolicyExceptionEffect>.Empty.Add(effect.Id, effect),
|
||||
ImmutableArray.Create(instance));
|
||||
var context = CreateContext("Critical", "internal", exceptions);
|
||||
|
||||
var result = evaluationService.Evaluate(document, context);
|
||||
|
||||
Assert.True(result.Matched);
|
||||
Assert.Equal("block_critical", result.RuleName);
|
||||
Assert.Equal("suppressed", result.Status);
|
||||
Assert.NotNull(result.AppliedException);
|
||||
Assert.Equal("exc-001", result.AppliedException!.ExceptionId);
|
||||
Assert.Equal("suppress-critical", result.AppliedException!.EffectId);
|
||||
Assert.Equal("blocked", result.AppliedException!.OriginalStatus);
|
||||
Assert.Equal("suppressed", result.AppliedException!.AppliedStatus);
|
||||
Assert.Equal("suppressed", result.Annotations["exception.status"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_ExceptionDowngradesSeverity()
|
||||
{
|
||||
var document = CompileBaseline();
|
||||
var effect = new PolicyExceptionEffect(
|
||||
Id: "downgrade-internet",
|
||||
Name: "Downgrade High Internet",
|
||||
Effect: PolicyExceptionEffectType.Downgrade,
|
||||
DowngradeSeverity: PolicySeverity.Medium,
|
||||
RequiredControlId: null,
|
||||
RoutingTemplate: null,
|
||||
MaxDurationDays: null,
|
||||
Description: null);
|
||||
var scope = PolicyEvaluationExceptionScope.Create(
|
||||
ruleNames: new[] { "escalate_high_internet" },
|
||||
severities: new[] { "High" },
|
||||
sources: new[] { "GHSA" });
|
||||
var instance = new PolicyEvaluationExceptionInstance(
|
||||
Id: "exc-200",
|
||||
EffectId: effect.Id,
|
||||
Scope: scope,
|
||||
CreatedAt: new DateTimeOffset(2025, 10, 2, 0, 0, 0, TimeSpan.Zero),
|
||||
Metadata: ImmutableDictionary<string, string>.Empty);
|
||||
var exceptions = new PolicyEvaluationExceptions(
|
||||
ImmutableDictionary<string, PolicyExceptionEffect>.Empty.Add(effect.Id, effect),
|
||||
ImmutableArray.Create(instance));
|
||||
var context = CreateContext("High", "internet", exceptions);
|
||||
|
||||
var result = evaluationService.Evaluate(document, context);
|
||||
|
||||
Assert.True(result.Matched);
|
||||
Assert.Equal("escalate_high_internet", result.RuleName);
|
||||
Assert.Equal("affected", result.Status);
|
||||
Assert.Equal("Medium", result.Severity);
|
||||
Assert.NotNull(result.AppliedException);
|
||||
Assert.Equal("Critical", result.AppliedException!.OriginalSeverity);
|
||||
Assert.Equal("Medium", result.AppliedException!.AppliedSeverity);
|
||||
Assert.Equal("Medium", result.Annotations["exception.severity"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_MoreSpecificExceptionWins()
|
||||
{
|
||||
var document = CompileBaseline();
|
||||
var suppressGlobal = new PolicyExceptionEffect(
|
||||
Id: "suppress-critical-global",
|
||||
Name: "Global Critical Suppress",
|
||||
Effect: PolicyExceptionEffectType.Suppress,
|
||||
DowngradeSeverity: null,
|
||||
RequiredControlId: null,
|
||||
RoutingTemplate: null,
|
||||
MaxDurationDays: null,
|
||||
Description: null);
|
||||
var suppressRule = new PolicyExceptionEffect(
|
||||
Id: "suppress-critical-rule",
|
||||
Name: "Rule Critical Suppress",
|
||||
Effect: PolicyExceptionEffectType.Suppress,
|
||||
DowngradeSeverity: null,
|
||||
RequiredControlId: null,
|
||||
RoutingTemplate: null,
|
||||
MaxDurationDays: null,
|
||||
Description: null);
|
||||
|
||||
var globalInstance = new PolicyEvaluationExceptionInstance(
|
||||
Id: "exc-global",
|
||||
EffectId: suppressGlobal.Id,
|
||||
Scope: PolicyEvaluationExceptionScope.Create(severities: new[] { "Critical" }),
|
||||
CreatedAt: new DateTimeOffset(2025, 9, 1, 0, 0, 0, TimeSpan.Zero),
|
||||
Metadata: ImmutableDictionary<string, string>.Empty);
|
||||
|
||||
var ruleInstance = new PolicyEvaluationExceptionInstance(
|
||||
Id: "exc-rule",
|
||||
EffectId: suppressRule.Id,
|
||||
Scope: PolicyEvaluationExceptionScope.Create(
|
||||
ruleNames: new[] { "block_critical" },
|
||||
severities: new[] { "Critical" }),
|
||||
CreatedAt: new DateTimeOffset(2025, 10, 5, 0, 0, 0, TimeSpan.Zero),
|
||||
Metadata: ImmutableDictionary<string, string>.Empty.Add("requestedBy", "alice"));
|
||||
|
||||
var effects = ImmutableDictionary<string, PolicyExceptionEffect>.Empty
|
||||
.Add(suppressGlobal.Id, suppressGlobal)
|
||||
.Add(suppressRule.Id, suppressRule);
|
||||
|
||||
var exceptions = new PolicyEvaluationExceptions(
|
||||
effects,
|
||||
ImmutableArray.Create(globalInstance, ruleInstance));
|
||||
|
||||
var context = CreateContext("Critical", "internal", exceptions);
|
||||
|
||||
var result = evaluationService.Evaluate(document, context);
|
||||
|
||||
Assert.True(result.Matched);
|
||||
Assert.Equal("suppressed", result.Status);
|
||||
Assert.NotNull(result.AppliedException);
|
||||
Assert.Equal("exc-rule", result.AppliedException!.ExceptionId);
|
||||
Assert.Equal("Rule Critical Suppress", result.AppliedException!.Metadata["effectName"]);
|
||||
Assert.Equal("alice", result.AppliedException!.Metadata["requestedBy"]);
|
||||
Assert.Equal("alice", result.Annotations["exception.meta.requestedBy"]);
|
||||
}
|
||||
|
||||
private PolicyIrDocument CompileBaseline()
|
||||
{
|
||||
var compilation = compiler.Compile(BaselinePolicy);
|
||||
Assert.True(compilation.Success, Describe(compilation.Diagnostics));
|
||||
return Assert.IsType<PolicyIrDocument>(compilation.Document);
|
||||
}
|
||||
|
||||
private static PolicyEvaluationContext CreateContext(string severity, string exposure, PolicyEvaluationExceptions? exceptions = null)
|
||||
{
|
||||
return new PolicyEvaluationContext(
|
||||
new PolicyEvaluationSeverity(severity),
|
||||
new PolicyEvaluationEnvironment(new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["exposure"] = exposure
|
||||
}.ToImmutableDictionary(StringComparer.OrdinalIgnoreCase)),
|
||||
new PolicyEvaluationAdvisory("GHSA", ImmutableDictionary<string, string>.Empty),
|
||||
PolicyEvaluationVexEvidence.Empty,
|
||||
new PolicyEvaluationSbom(ImmutableHashSet<string>.Empty),
|
||||
exceptions ?? PolicyEvaluationExceptions.Empty);
|
||||
}
|
||||
|
||||
private static string Describe(ImmutableArray<PolicyIssue> issues) =>
|
||||
string.Join(" | ", issues.Select(issue => $"{issue.Severity}:{issue.Code}:{issue.Message}"));
|
||||
}
|
||||
Reference in New Issue
Block a user