105 lines
3.8 KiB
C#
105 lines
3.8 KiB
C#
using System.Collections.Immutable;
|
|
using System.Linq;
|
|
using StellaOps.Policy;
|
|
using StellaOps.PolicyDsl;
|
|
using Xunit;
|
|
using Xunit.Sdk;
|
|
|
|
namespace StellaOps.Policy.Engine.Tests;
|
|
|
|
public sealed class PolicyCompilerTests
|
|
{
|
|
[Fact]
|
|
public void Compile_BaselinePolicy_Succeeds()
|
|
{
|
|
const string source = """
|
|
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."
|
|
}
|
|
}
|
|
""";
|
|
|
|
var compiler = new PolicyCompiler();
|
|
var result = compiler.Compile(source);
|
|
|
|
if (!result.Success)
|
|
{
|
|
throw new Xunit.Sdk.XunitException($"Compilation failed: {Describe(result.Diagnostics)}");
|
|
}
|
|
Assert.False(string.IsNullOrWhiteSpace(result.Checksum));
|
|
Assert.NotEmpty(result.CanonicalRepresentation);
|
|
Assert.All(result.Diagnostics, issue => Assert.NotEqual(PolicyIssueSeverity.Error, issue.Severity));
|
|
|
|
var document = Assert.IsType<PolicyIrDocument>(result.Document);
|
|
Assert.Equal("Baseline Production Policy", document.Name);
|
|
Assert.Equal("stella-dsl@1", document.Syntax);
|
|
Assert.Equal(4, document.Rules.Length);
|
|
Assert.Single(document.Profiles);
|
|
var firstAction = Assert.IsType<PolicyIrAssignmentAction>(document.Rules[0].ThenActions[0]);
|
|
Assert.Equal("status", firstAction.Target[0]);
|
|
}
|
|
|
|
[Fact]
|
|
public void Compile_MissingBecause_ReportsDiagnostic()
|
|
{
|
|
const string source = """
|
|
policy "Incomplete" syntax "stella-dsl@1" {
|
|
rule missing_because {
|
|
when true
|
|
then status := "suppressed"
|
|
}
|
|
}
|
|
""";
|
|
|
|
var compiler = new PolicyCompiler();
|
|
var result = compiler.Compile(source);
|
|
|
|
Assert.False(result.Success);
|
|
PolicyIssue diagnostic = result.Diagnostics.First(issue => issue.Code == "POLICY-DSL-PARSE-006");
|
|
Assert.Equal(PolicyIssueSeverity.Error, diagnostic.Severity);
|
|
}
|
|
|
|
private static string Describe(ImmutableArray<PolicyIssue> issues) =>
|
|
string.Join(" | ", issues.Select(issue => $"{issue.Severity}:{issue.Code}:{issue.Message}"));
|
|
}
|