This commit is contained in:
StellaOps Bot
2025-11-27 21:10:06 +02:00
parent cfa2274d31
commit 8abbf9574d
106 changed files with 7078 additions and 3197 deletions

View File

@@ -1,3 +1,4 @@
using Xunit;
using StellaOps.Policy.Engine.AdvisoryAI;
namespace StellaOps.Policy.Engine.Tests;

View File

@@ -1,3 +1,4 @@
using Xunit;
using StellaOps.Policy.Engine.Domain;
using StellaOps.Policy.Engine.Services;

View File

@@ -1,3 +1,4 @@
using Xunit;
using Microsoft.Extensions.Time.Testing;
using StellaOps.Policy.Engine.Ledger;
using StellaOps.Policy.Engine.Orchestration;

View File

@@ -1,3 +1,4 @@
using Xunit;
using Microsoft.Extensions.Time.Testing;
using StellaOps.Policy.Engine.Orchestration;

View File

@@ -1,3 +1,4 @@
using Xunit;
using System.Threading.Tasks;
using StellaOps.Policy.Engine.Overlay;
using StellaOps.Policy.Engine.Services;

View File

@@ -1,3 +1,4 @@
using Xunit;
using System.Text.Json;
using System.Threading.Tasks;
using StellaOps.Policy.Engine.Overlay;

View File

@@ -1,3 +1,4 @@
using Xunit;
using System.Linq;
using System.Threading.Tasks;
using StellaOps.Policy.Engine.Streaming;

View File

@@ -1,3 +1,4 @@
using Xunit;
using System.Collections.Immutable;
using System.Collections.Immutable;
using Microsoft.Extensions.Options;

View File

@@ -1,7 +1,7 @@
using System;
using Microsoft.Extensions.Options;
using StellaOps.Policy;
using StellaOps.Policy.Engine.Compilation;
using StellaOps.PolicyDsl;
using StellaOps.Policy.Engine.Options;
using StellaOps.Policy.Engine.Services;
using Xunit;

View File

@@ -1,7 +1,7 @@
using System.Collections.Immutable;
using System.Linq;
using StellaOps.Policy;
using StellaOps.Policy.Engine.Compilation;
using StellaOps.PolicyDsl;
using Xunit;
using Xunit.Sdk;

View File

@@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using StellaOps.Policy;
using StellaOps.Policy.Engine.Compilation;
using StellaOps.PolicyDsl;
using StellaOps.Policy.Engine.Evaluation;
using StellaOps.Policy.Engine.Services;
using Xunit;
@@ -51,26 +51,26 @@ policy "Baseline Production Policy" syntax "stella-dsl@1" {
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."
}
rule block_ruby_dev priority 4 {
when sbom.any_component(ruby.group("development") and ruby.declared_only())
then status := "blocked"
because "Development-only Ruby gems without install evidence cannot ship."
}
rule warn_ruby_git_sources {
when sbom.any_component(ruby.source("git"))
then warn message "Git-sourced Ruby gem present; review required."
because "Git-sourced Ruby dependencies require explicit review."
}
}
""";
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."
}
rule block_ruby_dev priority 4 {
when sbom.any_component(ruby.group("development") and ruby.declared_only())
then status := "blocked"
because "Development-only Ruby gems without install evidence cannot ship."
}
rule warn_ruby_git_sources {
when sbom.any_component(ruby.source("git"))
then warn message "Git-sourced Ruby gem present; review required."
because "Git-sourced Ruby dependencies require explicit review."
}
}
""";
private readonly PolicyCompiler compiler = new();
private readonly PolicyEvaluationService evaluationService = new();
@@ -125,11 +125,11 @@ policy "Baseline Production Policy" syntax "stella-dsl@1" {
public void Evaluate_WarnRuleEmitsWarning()
{
var document = CompileBaseline();
var tags = ImmutableHashSet.Create("runtime:eol");
var context = CreateContext("Medium", "internal") with
{
Sbom = new PolicyEvaluationSbom(tags)
};
var tags = ImmutableHashSet.Create("runtime:eol");
var context = CreateContext("Medium", "internal") with
{
Sbom = new PolicyEvaluationSbom(tags)
};
var result = evaluationService.Evaluate(document, context);
@@ -273,74 +273,74 @@ policy "Baseline Production Policy" syntax "stella-dsl@1" {
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"]);
}
[Fact]
public void Evaluate_RubyDevComponentBlocked()
{
var document = CompileBaseline();
var component = CreateRubyComponent(
name: "dev-only",
version: "1.0.0",
groups: "development;test",
declaredOnly: true,
source: "https://rubygems.org/",
capabilities: new[] { "exec" });
var context = CreateContext("Medium", "internal") with
{
Sbom = new PolicyEvaluationSbom(
ImmutableHashSet<string>.Empty.WithComparer(StringComparer.OrdinalIgnoreCase),
ImmutableArray.Create(component))
};
var result = evaluationService.Evaluate(document, context);
Assert.True(result.Matched);
Assert.Equal("block_ruby_dev", result.RuleName);
Assert.Equal("blocked", result.Status);
}
[Fact]
public void Evaluate_RubyGitComponentWarns()
{
var document = CompileBaseline();
var component = CreateRubyComponent(
name: "git-gem",
version: "0.5.0",
groups: "default",
declaredOnly: false,
source: "git:https://github.com/example/git-gem.git@0123456789abcdef0123456789abcdef01234567",
capabilities: Array.Empty<string>(),
schedulerCapabilities: new[] { "sidekiq" });
var context = CreateContext("Low", "internal") with
{
Sbom = new PolicyEvaluationSbom(
ImmutableHashSet<string>.Empty.WithComparer(StringComparer.OrdinalIgnoreCase),
ImmutableArray.Create(component))
};
var result = evaluationService.Evaluate(document, context);
Assert.True(result.Matched);
Assert.Equal("warn_ruby_git_sources", result.RuleName);
Assert.Equal("warned", result.Status);
Assert.Contains(result.Warnings, warning => warning.Contains("Git-sourced", StringComparison.OrdinalIgnoreCase));
}
private PolicyIrDocument CompileBaseline()
{
var compilation = compiler.Compile(BaselinePolicy);
if (!compilation.Success)
{
Console.WriteLine(Describe(compilation.Diagnostics));
}
Assert.True(compilation.Success, Describe(compilation.Diagnostics));
return Assert.IsType<PolicyIrDocument>(compilation.Document);
}
Assert.Equal("alice", result.AppliedException!.Metadata["requestedBy"]);
Assert.Equal("alice", result.Annotations["exception.meta.requestedBy"]);
}
[Fact]
public void Evaluate_RubyDevComponentBlocked()
{
var document = CompileBaseline();
var component = CreateRubyComponent(
name: "dev-only",
version: "1.0.0",
groups: "development;test",
declaredOnly: true,
source: "https://rubygems.org/",
capabilities: new[] { "exec" });
var context = CreateContext("Medium", "internal") with
{
Sbom = new PolicyEvaluationSbom(
ImmutableHashSet<string>.Empty.WithComparer(StringComparer.OrdinalIgnoreCase),
ImmutableArray.Create(component))
};
var result = evaluationService.Evaluate(document, context);
Assert.True(result.Matched);
Assert.Equal("block_ruby_dev", result.RuleName);
Assert.Equal("blocked", result.Status);
}
[Fact]
public void Evaluate_RubyGitComponentWarns()
{
var document = CompileBaseline();
var component = CreateRubyComponent(
name: "git-gem",
version: "0.5.0",
groups: "default",
declaredOnly: false,
source: "git:https://github.com/example/git-gem.git@0123456789abcdef0123456789abcdef01234567",
capabilities: Array.Empty<string>(),
schedulerCapabilities: new[] { "sidekiq" });
var context = CreateContext("Low", "internal") with
{
Sbom = new PolicyEvaluationSbom(
ImmutableHashSet<string>.Empty.WithComparer(StringComparer.OrdinalIgnoreCase),
ImmutableArray.Create(component))
};
var result = evaluationService.Evaluate(document, context);
Assert.True(result.Matched);
Assert.Equal("warn_ruby_git_sources", result.RuleName);
Assert.Equal("warned", result.Status);
Assert.Contains(result.Warnings, warning => warning.Contains("Git-sourced", StringComparison.OrdinalIgnoreCase));
}
private PolicyIrDocument CompileBaseline()
{
var compilation = compiler.Compile(BaselinePolicy);
if (!compilation.Success)
{
Console.WriteLine(Describe(compilation.Diagnostics));
}
Assert.True(compilation.Success, Describe(compilation.Diagnostics));
return Assert.IsType<PolicyIrDocument>(compilation.Document);
}
private static PolicyEvaluationContext CreateContext(string severity, string exposure, PolicyEvaluationExceptions? exceptions = null)
{
@@ -352,67 +352,67 @@ policy "Baseline Production Policy" syntax "stella-dsl@1" {
}.ToImmutableDictionary(StringComparer.OrdinalIgnoreCase)),
new PolicyEvaluationAdvisory("GHSA", ImmutableDictionary<string, string>.Empty),
PolicyEvaluationVexEvidence.Empty,
PolicyEvaluationSbom.Empty,
exceptions ?? PolicyEvaluationExceptions.Empty);
}
PolicyEvaluationSbom.Empty,
exceptions ?? PolicyEvaluationExceptions.Empty);
}
private static string Describe(ImmutableArray<PolicyIssue> issues) =>
string.Join(" | ", issues.Select(issue => $"{issue.Severity}:{issue.Code}:{issue.Message}"));
private static PolicyEvaluationComponent CreateRubyComponent(
string name,
string version,
string groups,
bool declaredOnly,
string source,
IEnumerable<string>? capabilities = null,
IEnumerable<string>? schedulerCapabilities = null)
{
var metadataBuilder = ImmutableDictionary.CreateBuilder<string, string>(StringComparer.OrdinalIgnoreCase);
if (!string.IsNullOrWhiteSpace(groups))
{
metadataBuilder["groups"] = groups;
}
metadataBuilder["declaredOnly"] = declaredOnly ? "true" : "false";
if (!string.IsNullOrWhiteSpace(source))
{
metadataBuilder["source"] = source.Trim();
}
if (capabilities is not null)
{
foreach (var capability in capabilities)
{
if (!string.IsNullOrWhiteSpace(capability))
{
metadataBuilder[$"capability.{capability.Trim()}"] = "true";
}
}
}
if (schedulerCapabilities is not null)
{
var schedulerList = string.Join(
';',
schedulerCapabilities
.Where(static s => !string.IsNullOrWhiteSpace(s))
.Select(static s => s.Trim()));
if (!string.IsNullOrWhiteSpace(schedulerList))
{
metadataBuilder["capability.scheduler"] = schedulerList;
}
}
metadataBuilder["lockfile"] = "Gemfile.lock";
return new PolicyEvaluationComponent(
name,
version,
"gem",
$"pkg:gem/{name}@{version}",
metadataBuilder.ToImmutable());
}
}
private static string Describe(ImmutableArray<PolicyIssue> issues) =>
string.Join(" | ", issues.Select(issue => $"{issue.Severity}:{issue.Code}:{issue.Message}"));
private static PolicyEvaluationComponent CreateRubyComponent(
string name,
string version,
string groups,
bool declaredOnly,
string source,
IEnumerable<string>? capabilities = null,
IEnumerable<string>? schedulerCapabilities = null)
{
var metadataBuilder = ImmutableDictionary.CreateBuilder<string, string>(StringComparer.OrdinalIgnoreCase);
if (!string.IsNullOrWhiteSpace(groups))
{
metadataBuilder["groups"] = groups;
}
metadataBuilder["declaredOnly"] = declaredOnly ? "true" : "false";
if (!string.IsNullOrWhiteSpace(source))
{
metadataBuilder["source"] = source.Trim();
}
if (capabilities is not null)
{
foreach (var capability in capabilities)
{
if (!string.IsNullOrWhiteSpace(capability))
{
metadataBuilder[$"capability.{capability.Trim()}"] = "true";
}
}
}
if (schedulerCapabilities is not null)
{
var schedulerList = string.Join(
';',
schedulerCapabilities
.Where(static s => !string.IsNullOrWhiteSpace(s))
.Select(static s => s.Trim()));
if (!string.IsNullOrWhiteSpace(schedulerList))
{
metadataBuilder["capability.scheduler"] = schedulerList;
}
}
metadataBuilder["lockfile"] = "Gemfile.lock";
return new PolicyEvaluationComponent(
name,
version,
"gem",
$"pkg:gem/{name}@{version}",
metadataBuilder.ToImmutable());
}
}

View File

@@ -1,3 +1,4 @@
using Xunit;
using System.Collections.Immutable;
using StellaOps.Policy.Engine.Domain;
using StellaOps.Policy.Engine.Services;

View File

@@ -1,3 +1,4 @@
using Xunit;
using Microsoft.Extensions.Time.Testing;
using StellaOps.Policy.Engine.Orchestration;

View File

@@ -1,3 +1,4 @@
using Xunit;
using Microsoft.Extensions.Time.Testing;
using StellaOps.Policy.Engine.Ledger;
using StellaOps.Policy.Engine.Orchestration;

View File

@@ -6,9 +6,25 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<UseConcelierTestInfra>false</UseConcelierTestInfra>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../StellaOps.Policy.Engine/StellaOps.Policy.Engine.csproj" />
</ItemGroup>

View File

@@ -1,3 +1,4 @@
using Xunit;
using StellaOps.Policy.Engine.TrustWeighting;
namespace StellaOps.Policy.Engine.Tests;

View File

@@ -1,3 +1,4 @@
using Xunit;
using Microsoft.Extensions.Time.Testing;
using StellaOps.Policy.Engine.Ledger;
using StellaOps.Policy.Engine.Orchestration;

View File

@@ -0,0 +1,183 @@
using FluentAssertions;
using StellaOps.PolicyDsl;
using Xunit;
namespace StellaOps.PolicyDsl.Tests;
/// <summary>
/// Tests for the policy DSL compiler.
/// </summary>
public class PolicyCompilerTests
{
private readonly PolicyCompiler _compiler = new();
[Fact]
public void Compile_MinimalPolicy_Succeeds()
{
// Arrange - rule name is an identifier, not a string; then block has no braces; := for assignment
var source = """
policy "test" syntax "stella-dsl@1" {
rule always priority 1 {
when true
then
severity := "info"
because "always applies"
}
}
""";
// Act
var result = _compiler.Compile(source);
// Assert
result.Success.Should().BeTrue(string.Join("; ", result.Diagnostics.Select(d => d.Message)));
result.Document.Should().NotBeNull();
result.Document!.Name.Should().Be("test");
result.Document.Syntax.Should().Be("stella-dsl@1");
result.Document.Rules.Should().HaveCount(1);
result.Checksum.Should().NotBeNullOrEmpty();
}
[Fact]
public void Compile_WithMetadata_ParsesCorrectly()
{
// Arrange
var source = """
policy "with-meta" syntax "stella-dsl@1" {
metadata {
version = "1.0.0"
author = "test"
}
rule r1 priority 1 {
when true
then
severity := "low"
because "required"
}
}
""";
// Act
var result = _compiler.Compile(source);
// Assert
result.Success.Should().BeTrue(string.Join("; ", result.Diagnostics.Select(d => d.Message)));
result.Document!.Metadata.Should().ContainKey("version");
result.Document.Metadata.Should().ContainKey("author");
}
[Fact]
public void Compile_WithProfile_ParsesCorrectly()
{
// Arrange
var source = """
policy "with-profile" syntax "stella-dsl@1" {
profile standard {
trust_score = 0.85
}
rule r1 priority 1 {
when true
then
severity := "low"
because "required"
}
}
""";
// Act
var result = _compiler.Compile(source);
// Assert
result.Success.Should().BeTrue(string.Join("; ", result.Diagnostics.Select(d => d.Message)));
result.Document!.Profiles.Should().HaveCount(1);
result.Document.Profiles[0].Name.Should().Be("standard");
}
[Fact]
public void Compile_EmptySource_ReturnsError()
{
// Arrange
var source = "";
// Act
var result = _compiler.Compile(source);
// Assert
result.Success.Should().BeFalse();
result.Diagnostics.Should().NotBeEmpty();
}
[Fact]
public void Compile_InvalidSyntax_ReturnsError()
{
// Arrange
var source = """
policy "bad" syntax "invalid@1" {
}
""";
// Act
var result = _compiler.Compile(source);
// Assert
result.Success.Should().BeFalse();
}
[Fact]
public void Compile_SameSource_ProducesSameChecksum()
{
// Arrange
var source = """
policy "deterministic" syntax "stella-dsl@1" {
rule r1 priority 1 {
when true
then
severity := "info"
because "always"
}
}
""";
// Act
var result1 = _compiler.Compile(source);
var result2 = _compiler.Compile(source);
// Assert
result1.Success.Should().BeTrue(string.Join("; ", result1.Diagnostics.Select(d => d.Message)));
result2.Success.Should().BeTrue(string.Join("; ", result2.Diagnostics.Select(d => d.Message)));
result1.Checksum.Should().Be(result2.Checksum);
}
[Fact]
public void Compile_DifferentSource_ProducesDifferentChecksum()
{
// Arrange
var source1 = """
policy "test1" syntax "stella-dsl@1" {
rule r1 priority 1 {
when true
then
severity := "info"
because "always"
}
}
""";
var source2 = """
policy "test2" syntax "stella-dsl@1" {
rule r1 priority 1 {
when true
then
severity := "info"
because "always"
}
}
""";
// Act
var result1 = _compiler.Compile(source1);
var result2 = _compiler.Compile(source2);
// Assert
result1.Checksum.Should().NotBe(result2.Checksum);
}
}

View File

@@ -0,0 +1,193 @@
using FluentAssertions;
using StellaOps.PolicyDsl;
using Xunit;
namespace StellaOps.PolicyDsl.Tests;
/// <summary>
/// Tests for the policy evaluation engine.
/// </summary>
public class PolicyEngineTests
{
private readonly PolicyEngineFactory _factory = new();
[Fact]
public void Evaluate_RuleMatches_ReturnsMatchedRules()
{
// Arrange
var source = """
policy "test" syntax "stella-dsl@1" {
rule critical_rule priority 100 {
when finding.severity == "critical"
then
severity := "critical"
because "critical finding detected"
}
}
""";
var result = _factory.CreateFromSource(source);
result.Engine.Should().NotBeNull(string.Join("; ", result.Diagnostics.Select(d => d.Message)));
var engine = result.Engine!;
var context = SignalContext.Builder()
.WithObject("finding", new Dictionary<string, object?> { ["severity"] = "critical" })
.Build();
// Act
var evalResult = engine.Evaluate(context);
// Assert
evalResult.MatchedRules.Should().Contain("critical_rule");
evalResult.PolicyChecksum.Should().NotBeNullOrEmpty();
}
[Fact]
public void Evaluate_RuleDoesNotMatch_ExecutesElseBranch()
{
// Arrange
var source = """
policy "test" syntax "stella-dsl@1" {
rule critical_only priority 100 {
when finding.severity == "critical"
then
severity := "critical"
else
severity := "info"
because "classify by severity"
}
}
""";
var result = _factory.CreateFromSource(source);
result.Engine.Should().NotBeNull(string.Join("; ", result.Diagnostics.Select(d => d.Message)));
var engine = result.Engine!;
var context = SignalContext.Builder()
.WithObject("finding", new Dictionary<string, object?> { ["severity"] = "low" })
.Build();
// Act
var evalResult = engine.Evaluate(context);
// Assert
evalResult.MatchedRules.Should().BeEmpty();
evalResult.Actions.Should().NotBeEmpty();
evalResult.Actions[0].WasElseBranch.Should().BeTrue();
}
[Fact]
public void Evaluate_MultipleRules_EvaluatesInPriorityOrder()
{
// Arrange
var source = """
policy "test" syntax "stella-dsl@1" {
rule low_priority priority 10 {
when true
then
severity := "low"
because "low priority rule"
}
rule high_priority priority 100 {
when true
then
severity := "high"
because "high priority rule"
}
}
""";
var result = _factory.CreateFromSource(source);
result.Engine.Should().NotBeNull(string.Join("; ", result.Diagnostics.Select(d => d.Message)));
var engine = result.Engine!;
var context = new SignalContext();
// Act
var evalResult = engine.Evaluate(context);
// Assert
evalResult.MatchedRules.Should().HaveCount(2);
evalResult.MatchedRules[0].Should().Be("high_priority");
evalResult.MatchedRules[1].Should().Be("low_priority");
}
[Fact]
public void Evaluate_WithAndCondition_MatchesWhenBothTrue()
{
// Arrange
var source = """
policy "test" syntax "stella-dsl@1" {
rule combined priority 100 {
when finding.severity == "critical" and reachability.state == "reachable"
then
severity := "critical"
because "critical and reachable"
}
}
""";
var result = _factory.CreateFromSource(source);
result.Engine.Should().NotBeNull(string.Join("; ", result.Diagnostics.Select(d => d.Message)));
var engine = result.Engine!;
var context = SignalContext.Builder()
.WithFinding("critical", 0.95m)
.WithReachability("reachable", 0.9m)
.Build();
// Act
var evalResult = engine.Evaluate(context);
// Assert
evalResult.MatchedRules.Should().Contain("combined");
}
[Fact]
public void Evaluate_WithOrCondition_MatchesWhenEitherTrue()
{
// Arrange
var source = """
policy "test" syntax "stella-dsl@1" {
rule either priority 100 {
when finding.severity == "critical" or finding.severity == "high"
then
severity := "elevated"
because "elevated severity"
}
}
""";
var result = _factory.CreateFromSource(source);
result.Engine.Should().NotBeNull(string.Join("; ", result.Diagnostics.Select(d => d.Message)));
var engine = result.Engine!;
var context = SignalContext.Builder()
.WithObject("finding", new Dictionary<string, object?> { ["severity"] = "high" })
.Build();
// Act
var evalResult = engine.Evaluate(context);
// Assert
evalResult.MatchedRules.Should().Contain("either");
}
[Fact]
public void Evaluate_WithNotCondition_InvertsResult()
{
// Arrange
var source = """
policy "test" syntax "stella-dsl@1" {
rule not_critical priority 100 {
when not finding.is_critical
then
severity := "low"
because "not critical"
}
}
""";
var result = _factory.CreateFromSource(source);
result.Engine.Should().NotBeNull(string.Join("; ", result.Diagnostics.Select(d => d.Message)));
var engine = result.Engine!;
var context = SignalContext.Builder()
.WithObject("finding", new Dictionary<string, object?> { ["is_critical"] = false })
.Build();
// Act
var evalResult = engine.Evaluate(context);
// Assert
evalResult.MatchedRules.Should().Contain("not_critical");
}
}

View File

@@ -0,0 +1,181 @@
using FluentAssertions;
using StellaOps.PolicyDsl;
using Xunit;
namespace StellaOps.PolicyDsl.Tests;
/// <summary>
/// Tests for the signal context API.
/// </summary>
public class SignalContextTests
{
[Fact]
public void Builder_WithSignal_SetsSignalValue()
{
// Arrange & Act
var context = SignalContext.Builder()
.WithSignal("test", "value")
.Build();
// Assert
context.GetSignal("test").Should().Be("value");
}
[Fact]
public void Builder_WithFlag_SetsBooleanSignal()
{
// Arrange & Act
var context = SignalContext.Builder()
.WithFlag("enabled")
.Build();
// Assert
context.GetSignal<bool>("enabled").Should().BeTrue();
}
[Fact]
public void Builder_WithNumber_SetsDecimalSignal()
{
// Arrange & Act
var context = SignalContext.Builder()
.WithNumber("score", 0.95m)
.Build();
// Assert
context.GetSignal<decimal>("score").Should().Be(0.95m);
}
[Fact]
public void Builder_WithString_SetsStringSignal()
{
// Arrange & Act
var context = SignalContext.Builder()
.WithString("name", "test")
.Build();
// Assert
context.GetSignal<string>("name").Should().Be("test");
}
[Fact]
public void Builder_WithFinding_SetsNestedFindingObject()
{
// Arrange & Act
var context = SignalContext.Builder()
.WithFinding("critical", 0.95m, "CVE-2024-1234")
.Build();
// Assert
context.HasSignal("finding").Should().BeTrue();
var finding = context.GetSignal("finding") as IDictionary<string, object?>;
finding.Should().NotBeNull();
finding!["severity"].Should().Be("critical");
finding["confidence"].Should().Be(0.95m);
finding["cve_id"].Should().Be("CVE-2024-1234");
}
[Fact]
public void Builder_WithReachability_SetsNestedReachabilityObject()
{
// Arrange & Act
var context = SignalContext.Builder()
.WithReachability("reachable", 0.9m, hasRuntimeEvidence: true)
.Build();
// Assert
context.HasSignal("reachability").Should().BeTrue();
var reachability = context.GetSignal("reachability") as IDictionary<string, object?>;
reachability.Should().NotBeNull();
reachability!["state"].Should().Be("reachable");
reachability["confidence"].Should().Be(0.9m);
reachability["has_runtime_evidence"].Should().Be(true);
}
[Fact]
public void Builder_WithTrustScore_SetsTrustSignals()
{
// Arrange & Act
var context = SignalContext.Builder()
.WithTrustScore(0.85m, verified: true)
.Build();
// Assert
context.GetSignal<decimal>("trust_score").Should().Be(0.85m);
context.GetSignal<bool>("trust_verified").Should().BeTrue();
}
[Fact]
public void SetSignal_UpdatesExistingValue()
{
// Arrange
var context = new SignalContext();
context.SetSignal("key", "value1");
// Act
context.SetSignal("key", "value2");
// Assert
context.GetSignal("key").Should().Be("value2");
}
[Fact]
public void RemoveSignal_RemovesExistingSignal()
{
// Arrange
var context = new SignalContext();
context.SetSignal("key", "value");
// Act
context.RemoveSignal("key");
// Assert
context.HasSignal("key").Should().BeFalse();
}
[Fact]
public void Clone_CreatesIndependentCopy()
{
// Arrange
var original = SignalContext.Builder()
.WithSignal("key", "value")
.Build();
// Act
var clone = original.Clone();
clone.SetSignal("key", "modified");
// Assert
original.GetSignal("key").Should().Be("value");
clone.GetSignal("key").Should().Be("modified");
}
[Fact]
public void SignalNames_ReturnsAllSignalKeys()
{
// Arrange
var context = SignalContext.Builder()
.WithSignal("a", 1)
.WithSignal("b", 2)
.WithSignal("c", 3)
.Build();
// Act & Assert
context.SignalNames.Should().BeEquivalentTo(new[] { "a", "b", "c" });
}
[Fact]
public void Signals_ReturnsReadOnlyDictionary()
{
// Arrange
var context = SignalContext.Builder()
.WithSignal("key", "value")
.Build();
// Act
var signals = context.Signals;
// Assert
signals.Should().ContainKey("key");
signals["key"].Should().Be("value");
}
}

View File

@@ -0,0 +1,35 @@
<?xml version='1.0' encoding='utf-8'?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<!-- Disable Concelier test infra to avoid duplicate package references -->
<UseConcelierTestInfra>false</UseConcelierTestInfra>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../StellaOps.PolicyDsl/StellaOps.PolicyDsl.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="TestData\*.dsl">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,56 @@
// Default reachability-aware policy
// syntax: stella-dsl@1
policy "default-reachability" syntax "stella-dsl@1" {
metadata {
version = "1.0.0"
description = "Default policy with reachability-aware rules"
author = "StellaOps"
}
settings {
default_action = "warn"
fail_on_critical = true
}
profile standard {
trust_score = 0.85
}
// Critical vulnerabilities with confirmed reachability
rule critical_reachable priority 100 {
when finding.severity == "critical" and reachability.state == "reachable"
then
severity := "critical"
annotate finding.priority := "immediate"
escalate to "security-team" when reachability.confidence > 0.9
because "Critical vulnerabilities with confirmed reachability require immediate action"
}
// High severity with runtime evidence
rule high_with_evidence priority 90 {
when finding.severity == "high" and reachability.has_runtime_evidence
then
severity := "high"
annotate finding.evidence := "runtime-confirmed"
else
defer until "reachability-assessment"
because "High severity findings need runtime evidence for prioritization"
}
// Low severity unreachable can be ignored
rule low_unreachable priority 50 {
when finding.severity == "low" and reachability.state == "unreachable"
then
ignore until "next-scan" because "Low severity unreachable code"
because "Low severity unreachable vulnerabilities can be safely deferred"
}
// Unknown reachability requires VEX
rule unknown_reachability priority 40 {
when not reachability.state
then
warn message "Reachability assessment pending"
because "Unknown reachability requires manual assessment"
}
}

View File

@@ -0,0 +1,11 @@
// Minimal valid policy
// syntax: stella-dsl@1
policy "minimal" syntax "stella-dsl@1" {
rule always_pass priority 1 {
when true
then
severity := "info"
because "always applies"
}
}