using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using StellaOps.Policy; using StellaOps.PolicyDsl; 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." } 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(); [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.Empty); var exceptions = new PolicyEvaluationExceptions( ImmutableDictionary.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.Empty); var exceptions = new PolicyEvaluationExceptions( ImmutableDictionary.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.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.Empty.Add("requestedBy", "alice")); var effects = ImmutableDictionary.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"]); } [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.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(), schedulerCapabilities: new[] { "sidekiq" }); var context = CreateContext("Low", "internal") with { Sbom = new PolicyEvaluationSbom( ImmutableHashSet.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(compilation.Document); } private static PolicyEvaluationContext CreateContext(string severity, string exposure, PolicyEvaluationExceptions? exceptions = null) { return new PolicyEvaluationContext( new PolicyEvaluationSeverity(severity), new PolicyEvaluationEnvironment(new Dictionary(StringComparer.OrdinalIgnoreCase) { ["exposure"] = exposure }.ToImmutableDictionary(StringComparer.OrdinalIgnoreCase)), new PolicyEvaluationAdvisory("GHSA", ImmutableDictionary.Empty), PolicyEvaluationVexEvidence.Empty, PolicyEvaluationSbom.Empty, exceptions ?? PolicyEvaluationExceptions.Empty); } private static string Describe(ImmutableArray 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? capabilities = null, IEnumerable? schedulerCapabilities = null) { var metadataBuilder = ImmutableDictionary.CreateBuilder(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 PolicyEvaluationComponent CreateMacOsComponent( string bundleId, string version, bool sandboxed = false, bool hardenedRuntime = false, string? teamId = null, string? codeResourcesHash = null, IEnumerable? categories = null, IEnumerable? highRiskEntitlements = null, string? pkgutilIdentifier = null) { var metadataBuilder = ImmutableDictionary.CreateBuilder(StringComparer.OrdinalIgnoreCase); metadataBuilder["macos:bundle_id"] = bundleId; metadataBuilder["macos:sandboxed"] = sandboxed ? "true" : "false"; metadataBuilder["macos:hardened_runtime"] = hardenedRuntime ? "true" : "false"; if (!string.IsNullOrWhiteSpace(teamId)) { metadataBuilder["macos:team_id"] = teamId; } if (!string.IsNullOrWhiteSpace(codeResourcesHash)) { metadataBuilder["macos:code_resources_hash"] = codeResourcesHash; } if (categories is not null && categories.Any()) { metadataBuilder["macos:capability_categories"] = string.Join(",", categories); } if (highRiskEntitlements is not null && highRiskEntitlements.Any()) { metadataBuilder["macos:high_risk_entitlements"] = string.Join(",", highRiskEntitlements); } if (!string.IsNullOrWhiteSpace(pkgutilIdentifier)) { metadataBuilder["pkgutil:identifier"] = pkgutilIdentifier; } return new PolicyEvaluationComponent( bundleId.Split('.').Last(), version, "macos-bundle", $"pkg:generic/macos-app/{bundleId}@{version}", metadataBuilder.ToImmutable()); } #region macOS Policy Predicate Tests private static readonly string MacOsPolicy = """ policy "macOS Security Policy" syntax "stella-dsl@1" { metadata { description = "Enforce macOS bundle security requirements." tags = ["macos","security"] } rule block_unsigned_apps priority 3 { when sbom.any_component(not macos.signed) then status := "blocked" because "Unsigned macOS applications are not permitted." } rule warn_high_risk_entitlements priority 4 { when sbom.any_component(macos.high_risk_entitlements) then warn message "Application requests high-risk entitlements." because "High-risk entitlements require review." } rule require_hardened_runtime priority 5 { when sbom.any_component(macos.signed and not macos.hardened_runtime) then warn message "Application does not use hardened runtime." because "Hardened runtime is recommended for security." } rule block_unsandboxed_apps priority 2 { when sbom.any_component(not macos.sandboxed) then warn message "Application is not sandboxed." because "App sandbox provides security isolation." } rule block_camera_microphone priority 1 { when sbom.any_component(macos.category_any(["camera", "microphone"])) then warn message "Application accesses camera or microphone." because "Camera/microphone access requires review." } } """; [Fact] public void Evaluate_MacOs_UnsignedAppBlocked() { var document = compiler.Compile(MacOsPolicy); Assert.True(document.Success); var ir = Assert.IsType(document.Document); // Unsigned but sandboxed app to avoid matching the unsandboxed rule first var component = CreateMacOsComponent( bundleId: "com.unknown.UnsignedApp", version: "1.0.0", sandboxed: true, // Sandboxed to avoid matching block_unsandboxed_apps first hardenedRuntime: false, teamId: null, codeResourcesHash: null); var context = CreateContext("Medium", "internal") with { Sbom = new PolicyEvaluationSbom( ImmutableHashSet.Empty, ImmutableArray.Create(component)) }; var result = evaluationService.Evaluate(ir, context); Assert.True(result.Matched); Assert.Equal("block_unsigned_apps", result.RuleName); Assert.Equal("blocked", result.Status); } [Fact] public void Evaluate_MacOs_SignedAppPasses() { var document = compiler.Compile(MacOsPolicy); Assert.True(document.Success); var ir = Assert.IsType(document.Document); var component = CreateMacOsComponent( bundleId: "com.apple.Safari", version: "17.1", sandboxed: true, hardenedRuntime: true, teamId: "APPLE123", codeResourcesHash: "sha256:abc123"); var context = CreateContext("Medium", "internal") with { Sbom = new PolicyEvaluationSbom( ImmutableHashSet.Empty, ImmutableArray.Create(component)) }; var result = evaluationService.Evaluate(ir, context); // No blocking rules should match for a properly signed and sandboxed app Assert.False(result.Matched && result.Status == "blocked"); } [Fact] public void Evaluate_MacOs_HighRiskEntitlementsWarns() { var document = compiler.Compile(MacOsPolicy); Assert.True(document.Success); var ir = Assert.IsType(document.Document); // App with high-risk entitlements but no camera/microphone categories // to avoid matching block_camera_microphone rule first var component = CreateMacOsComponent( bundleId: "com.example.AutomationApp", version: "2.0.0", sandboxed: true, hardenedRuntime: true, teamId: "TEAM123", codeResourcesHash: "sha256:def456", categories: new[] { "network", "automation" }, highRiskEntitlements: new[] { "com.apple.security.automation.apple-events" }); var context = CreateContext("Medium", "internal") with { Sbom = new PolicyEvaluationSbom( ImmutableHashSet.Empty, ImmutableArray.Create(component)) }; var result = evaluationService.Evaluate(ir, context); Assert.True(result.Matched); Assert.Equal("warn_high_risk_entitlements", result.RuleName); Assert.Equal("warned", result.Status); } [Fact] public void Evaluate_MacOs_CategoryMatchesCameraAccess() { var document = compiler.Compile(MacOsPolicy); Assert.True(document.Success); var ir = Assert.IsType(document.Document); var component = CreateMacOsComponent( bundleId: "com.example.VideoChat", version: "1.5.0", sandboxed: true, hardenedRuntime: true, teamId: "TEAM456", codeResourcesHash: "sha256:ghi789", categories: new[] { "camera", "microphone", "network" }); var context = CreateContext("Medium", "internal") with { Sbom = new PolicyEvaluationSbom( ImmutableHashSet.Empty, ImmutableArray.Create(component)) }; var result = evaluationService.Evaluate(ir, context); // Should match camera/microphone warning rule Assert.True(result.Matched); // Either high_risk or camera_microphone rule should match Assert.True( result.RuleName == "block_camera_microphone" || result.RuleName == "warn_high_risk_entitlements" || result.Status == "warned"); } [Fact] public void Evaluate_MacOs_HardenedRuntimeWarnsWhenMissing() { var document = compiler.Compile(MacOsPolicy); Assert.True(document.Success); var ir = Assert.IsType(document.Document); var component = CreateMacOsComponent( bundleId: "com.example.LegacyApp", version: "3.0.0", sandboxed: true, hardenedRuntime: false, teamId: "TEAM789", codeResourcesHash: "sha256:jkl012"); var context = CreateContext("Medium", "internal") with { Sbom = new PolicyEvaluationSbom( ImmutableHashSet.Empty, ImmutableArray.Create(component)) }; var result = evaluationService.Evaluate(ir, context); Assert.True(result.Matched); Assert.Equal("require_hardened_runtime", result.RuleName); Assert.Equal("warned", result.Status); } #endregion }