up
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-11-28 09:40:40 +02:00
parent 1c6730a1d2
commit 05da719048
206 changed files with 34741 additions and 1751 deletions

View File

@@ -0,0 +1,468 @@
using System.Collections.Immutable;
using FluentAssertions;
using StellaOps.Policy.Engine.Compilation;
using StellaOps.PolicyDsl;
using Xunit;
namespace StellaOps.Policy.Engine.Tests.Compilation;
public sealed class PolicyMetadataExtractorTests
{
private readonly PolicyMetadataExtractor _extractor = new();
private readonly PolicyCompiler _compiler = new();
[Fact]
public void Extract_EmptyPolicy_ReturnsEmptyMetadata()
{
// Arrange
var source = """
policy "Empty" syntax "stella-dsl@1" {
rule empty_rule priority 1 {
when true
then status := "test"
because "Test rule"
}
}
""";
var result = _compiler.Compile(source);
result.Success.Should().BeTrue();
// Act
var metadata = _extractor.Extract(result.Document!, result.CanonicalRepresentation);
// Assert
metadata.Should().NotBeNull();
metadata.SymbolTable.Should().NotBeNull();
metadata.RuleIndex.Should().NotBeNull();
metadata.Documentation.Should().NotBeNull();
metadata.CoverageMetadata.Should().NotBeNull();
metadata.Hashes.Should().NotBeNull();
}
[Fact]
public void Extract_SymbolTable_ContainsRuleSymbols()
{
// Arrange
var source = """
policy "SymbolTest" syntax "stella-dsl@1" {
rule severity_check priority 1 {
when advisory.severity == "critical"
then status := "blocked"
because "Block critical vulnerabilities"
}
rule low_severity priority 2 {
when advisory.severity == "low"
then status := "allowed"
because "Allow low severity"
}
}
""";
var result = _compiler.Compile(source);
result.Success.Should().BeTrue();
// Act
var metadata = _extractor.Extract(result.Document!, result.CanonicalRepresentation);
// Assert
metadata.SymbolTable.Symbols.Should().Contain(s => s.Name == "severity_check" && s.Kind == PolicySymbolKind.Rule);
metadata.SymbolTable.Symbols.Should().Contain(s => s.Name == "low_severity" && s.Kind == PolicySymbolKind.Rule);
}
[Fact]
public void Extract_SymbolTable_TracksIdentifierReferences()
{
// Arrange
var source = """
policy "RefTest" syntax "stella-dsl@1" {
rule check priority 1 {
when advisory.severity == "critical" and component.ecosystem == "npm"
then status := "blocked"
because "Test"
}
}
""";
var result = _compiler.Compile(source);
result.Success.Should().BeTrue();
// Act
var metadata = _extractor.Extract(result.Document!, result.CanonicalRepresentation);
// Assert
metadata.SymbolTable.ReferencesByName.Should().ContainKey("advisory");
metadata.SymbolTable.ReferencesByName.Should().ContainKey("component");
}
[Fact]
public void Extract_SymbolTable_ContainsBuiltInFunctions()
{
// Arrange
var source = """
policy "FuncTest" syntax "stella-dsl@1" {
rule check priority 1 {
when true
then status := "test"
because "Test"
}
}
""";
var result = _compiler.Compile(source);
result.Success.Should().BeTrue();
// Act
var metadata = _extractor.Extract(result.Document!, result.CanonicalRepresentation);
// Assert
metadata.SymbolTable.BuiltInFunctions.Should().NotBeEmpty();
metadata.SymbolTable.BuiltInFunctions.Should().Contain(f => f.Name == "contains");
metadata.SymbolTable.BuiltInFunctions.Should().Contain(f => f.Name == "startsWith");
metadata.SymbolTable.BuiltInFunctions.Should().Contain(f => f.Name == "matches");
metadata.SymbolTable.BuiltInFunctions.Should().Contain(f => f.Name == "now");
}
[Fact]
public void Extract_RuleIndex_IndexesRulesByName()
{
// Arrange
var source = """
policy "IndexTest" syntax "stella-dsl@1" {
rule rule_a priority 1 {
when true
then status := "a"
because "A"
}
rule rule_b priority 2 {
when true
then status := "b"
because "B"
}
}
""";
var result = _compiler.Compile(source);
result.Success.Should().BeTrue();
// Act
var metadata = _extractor.Extract(result.Document!, result.CanonicalRepresentation);
// Assert
metadata.RuleIndex.ByName.Should().ContainKey("rule_a");
metadata.RuleIndex.ByName.Should().ContainKey("rule_b");
metadata.RuleIndex.ByName["rule_a"].Priority.Should().Be(1);
metadata.RuleIndex.ByName["rule_b"].Priority.Should().Be(2);
}
[Fact]
public void Extract_RuleIndex_IndexesRulesByPriority()
{
// Arrange
var source = """
policy "PriorityTest" syntax "stella-dsl@1" {
rule high_priority priority 1 {
when true
then status := "high"
because "High"
}
rule also_high priority 1 {
when true
then status := "also_high"
because "Also high"
}
rule low_priority priority 10 {
when true
then status := "low"
because "Low"
}
}
""";
var result = _compiler.Compile(source);
result.Success.Should().BeTrue();
// Act
var metadata = _extractor.Extract(result.Document!, result.CanonicalRepresentation);
// Assert
metadata.RuleIndex.ByPriority.Should().ContainKey(1);
metadata.RuleIndex.ByPriority.Should().ContainKey(10);
metadata.RuleIndex.ByPriority[1].Should().HaveCount(2);
metadata.RuleIndex.ByPriority[10].Should().HaveCount(1);
}
[Fact]
public void Extract_RuleIndex_TracksActionTypes()
{
// Arrange
var source = """
policy "ActionTest" syntax "stella-dsl@1" {
rule mixed_actions priority 1 {
when advisory.severity == "critical"
then status := "blocked"; warn message "blocking"
else status := "allowed"
because "Mixed actions"
}
}
""";
var result = _compiler.Compile(source);
result.Success.Should().BeTrue();
// Act
var metadata = _extractor.Extract(result.Document!, result.CanonicalRepresentation);
// Assert
metadata.RuleIndex.ActionTypes.Should().Contain("assign");
metadata.RuleIndex.ActionTypes.Should().Contain("warn");
}
[Fact]
public void Extract_Documentation_ExtractsMetadata()
{
// Arrange
var source = """
policy "DocTest" syntax "stella-dsl@1" {
metadata {
description = "A test policy for documentation"
author = "Test Author"
tags = ["security", "compliance"]
}
rule check priority 1 {
when true
then status := "test"
because "Test rule"
}
}
""";
var result = _compiler.Compile(source);
result.Success.Should().BeTrue();
// Act
var metadata = _extractor.Extract(result.Document!, result.CanonicalRepresentation);
// Assert
metadata.Documentation.PolicyDescription.Should().Be("A test policy for documentation");
metadata.Documentation.Author.Should().Be("Test Author");
metadata.Documentation.Tags.Should().Contain("security");
metadata.Documentation.Tags.Should().Contain("compliance");
}
[Fact]
public void Extract_Documentation_ExtractsRuleJustifications()
{
// Arrange
var source = """
policy "JustificationTest" syntax "stella-dsl@1" {
rule critical_block priority 1 {
when advisory.severity == "critical"
then status := "blocked"
because "Critical vulnerabilities must be blocked immediately"
}
}
""";
var result = _compiler.Compile(source);
result.Success.Should().BeTrue();
// Act
var metadata = _extractor.Extract(result.Document!, result.CanonicalRepresentation);
// Assert
metadata.Documentation.RuleDocumentation.Should().HaveCount(1);
metadata.Documentation.RuleDocumentation[0].Justification.Should().Be("Critical vulnerabilities must be blocked immediately");
}
[Fact]
public void Extract_CoverageMetadata_TracksCoveragePoints()
{
// Arrange
var source = """
policy "CoverageTest" syntax "stella-dsl@1" {
rule with_else priority 1 {
when advisory.severity == "critical"
then status := "blocked"
else status := "allowed"
because "Test coverage"
}
}
""";
var result = _compiler.Compile(source);
result.Success.Should().BeTrue();
// Act
var metadata = _extractor.Extract(result.Document!, result.CanonicalRepresentation);
// Assert
metadata.CoverageMetadata.TotalRules.Should().Be(1);
metadata.CoverageMetadata.Rules[0].HasElseBranch.Should().BeTrue();
metadata.CoverageMetadata.Rules[0].CoveragePoints.Should().Contain("with_else:condition");
metadata.CoverageMetadata.Rules[0].CoveragePoints.Should().Contain("with_else:then");
metadata.CoverageMetadata.Rules[0].CoveragePoints.Should().Contain("with_else:else");
}
[Fact]
public void Extract_CoverageMetadata_GeneratesCoveragePaths()
{
// Arrange
var source = """
policy "PathTest" syntax "stella-dsl@1" {
rule rule_1 priority 1 {
when true
then status := "1"
because "Rule 1"
}
rule rule_2 priority 2 {
when true
then status := "2"
because "Rule 2"
}
}
""";
var result = _compiler.Compile(source);
result.Success.Should().BeTrue();
// Act
var metadata = _extractor.Extract(result.Document!, result.CanonicalRepresentation);
// Assert
// 2 rules = 4 possible paths (2^2)
metadata.CoverageMetadata.CoveragePaths.Should().HaveCount(4);
metadata.CoverageMetadata.CoveragePaths.Should().OnlyContain(p => p.RuleSequence.Length == 2);
}
[Fact]
public void Extract_Hashes_AreConsistentForSameInput()
{
// Arrange
var source = """
policy "HashTest" syntax "stella-dsl@1" {
rule check priority 1 {
when true
then status := "test"
because "Test"
}
}
""";
var result = _compiler.Compile(source);
result.Success.Should().BeTrue();
// Act
var metadata1 = _extractor.Extract(result.Document!, result.CanonicalRepresentation);
var metadata2 = _extractor.Extract(result.Document!, result.CanonicalRepresentation);
// Assert
metadata1.Hashes.ContentHash.Should().Be(metadata2.Hashes.ContentHash);
metadata1.Hashes.StructureHash.Should().Be(metadata2.Hashes.StructureHash);
metadata1.Hashes.OrderingHash.Should().Be(metadata2.Hashes.OrderingHash);
metadata1.Hashes.IdentityHash.Should().Be(metadata2.Hashes.IdentityHash);
}
[Fact]
public void Extract_Hashes_DifferForDifferentPolicies()
{
// Arrange
var source1 = """
policy "Policy1" syntax "stella-dsl@1" {
rule check priority 1 {
when true
then status := "1"
because "Test 1"
}
}
""";
var source2 = """
policy "Policy2" syntax "stella-dsl@1" {
rule check priority 1 {
when true
then status := "2"
because "Test 2"
}
}
""";
var result1 = _compiler.Compile(source1);
var result2 = _compiler.Compile(source2);
result1.Success.Should().BeTrue();
result2.Success.Should().BeTrue();
// Act
var metadata1 = _extractor.Extract(result1.Document!, result1.CanonicalRepresentation);
var metadata2 = _extractor.Extract(result2.Document!, result2.CanonicalRepresentation);
// Assert
metadata1.Hashes.ContentHash.Should().NotBe(metadata2.Hashes.ContentHash);
metadata1.Hashes.IdentityHash.Should().NotBe(metadata2.Hashes.IdentityHash);
}
[Fact]
public void Extract_SymbolTable_TracksVariableDefinitions()
{
// Arrange
var source = """
policy "VarTest" syntax "stella-dsl@1" {
rule assign_var priority 1 {
when advisory.severity == "critical"
then status := "blocked"; reason := "Critical vuln"
because "Test"
}
}
""";
var result = _compiler.Compile(source);
result.Success.Should().BeTrue();
// Act
var metadata = _extractor.Extract(result.Document!, result.CanonicalRepresentation);
// Assert
metadata.SymbolTable.Variables.Should().Contain(v => v.Name == "status");
metadata.SymbolTable.Variables.Should().Contain(v => v.Name == "reason");
}
[Fact]
public void Extract_RuleIndex_TracksReferencedIdentifiers()
{
// Arrange
var source = """
policy "RefIdentTest" syntax "stella-dsl@1" {
rule check priority 1 {
when advisory.severity == "critical" and component.ecosystem == "npm"
then status := "blocked"
because "Test"
}
}
""";
var result = _compiler.Compile(source);
result.Success.Should().BeTrue();
// Act
var metadata = _extractor.Extract(result.Document!, result.CanonicalRepresentation);
// Assert
metadata.RuleIndex.UsedIdentifiers.Should().Contain("advisory");
metadata.RuleIndex.UsedIdentifiers.Should().Contain("component");
}
[Fact]
public void Extract_CoverageMetadata_CountsActionTypes()
{
// Arrange
var source = """
policy "ActionCountTest" syntax "stella-dsl@1" {
rule rule1 priority 1 {
when true
then status := "a"; warn message "warning"
because "Rule 1"
}
rule rule2 priority 2 {
when true
then status := "b"
because "Rule 2"
}
}
""";
var result = _compiler.Compile(source);
result.Success.Should().BeTrue();
// Act
var metadata = _extractor.Extract(result.Document!, result.CanonicalRepresentation);
// Assert
metadata.CoverageMetadata.ActionTypeCounts.Should().ContainKey("assign");
metadata.CoverageMetadata.ActionTypeCounts["assign"].Should().Be(2);
metadata.CoverageMetadata.ActionTypeCounts.Should().ContainKey("warn");
metadata.CoverageMetadata.ActionTypeCounts["warn"].Should().Be(1);
}
}

View File

@@ -0,0 +1,430 @@
using FluentAssertions;
using StellaOps.Policy.Engine.DeterminismGuard;
using Xunit;
namespace StellaOps.Policy.Engine.Tests.DeterminismGuard;
public sealed class DeterminismGuardTests
{
#region ProhibitedPatternAnalyzer Tests
[Fact]
public void AnalyzeSource_DetectsDateTimeNow()
{
// Arrange
var analyzer = new ProhibitedPatternAnalyzer();
var source = """
public class Test
{
public DateTime GetTime() => DateTime.Now;
}
""";
// Act
var result = analyzer.AnalyzeSource(source, "test.cs", DeterminismGuardOptions.Default);
// Assert
result.Passed.Should().BeFalse();
result.Violations.Should().ContainSingle(v =>
v.ViolationType == "DateTime.Now" &&
v.Category == DeterminismViolationCategory.WallClock);
}
[Fact]
public void AnalyzeSource_DetectsDateTimeUtcNow()
{
var analyzer = new ProhibitedPatternAnalyzer();
var source = "var now = DateTime.UtcNow;";
var result = analyzer.AnalyzeSource(source, "test.cs", DeterminismGuardOptions.Default);
result.Violations.Should().ContainSingle(v =>
v.ViolationType == "DateTime.UtcNow");
}
[Fact]
public void AnalyzeSource_DetectsRandomClass()
{
var analyzer = new ProhibitedPatternAnalyzer();
var source = "var rng = new Random();";
var result = analyzer.AnalyzeSource(source, "test.cs", DeterminismGuardOptions.Default);
result.Violations.Should().ContainSingle(v =>
v.ViolationType == "Random" &&
v.Category == DeterminismViolationCategory.RandomNumber);
}
[Fact]
public void AnalyzeSource_DetectsGuidNewGuid()
{
var analyzer = new ProhibitedPatternAnalyzer();
var source = "var id = Guid.NewGuid();";
var result = analyzer.AnalyzeSource(source, "test.cs", DeterminismGuardOptions.Default);
result.Violations.Should().ContainSingle(v =>
v.ViolationType == "Guid.NewGuid" &&
v.Category == DeterminismViolationCategory.GuidGeneration);
}
[Fact]
public void AnalyzeSource_DetectsHttpClient()
{
var analyzer = new ProhibitedPatternAnalyzer();
var source = "private readonly HttpClient _client = new();";
var result = analyzer.AnalyzeSource(source, "test.cs", DeterminismGuardOptions.Default);
result.Violations.Should().ContainSingle(v =>
v.ViolationType == "HttpClient" &&
v.Category == DeterminismViolationCategory.NetworkAccess &&
v.Severity == DeterminismViolationSeverity.Critical);
}
[Fact]
public void AnalyzeSource_DetectsFileOperations()
{
var analyzer = new ProhibitedPatternAnalyzer();
var source = """
var content = File.ReadAllText("test.txt");
File.WriteAllText("out.txt", content);
""";
var result = analyzer.AnalyzeSource(source, "test.cs", DeterminismGuardOptions.Default);
result.Violations.Should().HaveCount(2);
result.Violations.Should().Contain(v => v.ViolationType == "File.Read");
result.Violations.Should().Contain(v => v.ViolationType == "File.Write");
}
[Fact]
public void AnalyzeSource_DetectsEnvironmentVariableAccess()
{
var analyzer = new ProhibitedPatternAnalyzer();
var source = "var path = Environment.GetEnvironmentVariable(\"PATH\");";
var result = analyzer.AnalyzeSource(source, "test.cs", DeterminismGuardOptions.Default);
result.Violations.Should().ContainSingle(v =>
v.ViolationType == "Environment.GetEnvironmentVariable");
}
[Fact]
public void AnalyzeSource_IgnoresComments()
{
var analyzer = new ProhibitedPatternAnalyzer();
var source = """
// DateTime.Now is not allowed
/* DateTime.UtcNow either */
* Random comment
""";
var result = analyzer.AnalyzeSource(source, "test.cs", DeterminismGuardOptions.Default);
result.Violations.Should().BeEmpty();
result.Passed.Should().BeTrue();
}
[Fact]
public void AnalyzeSource_RespectsExcludePatterns()
{
var analyzer = new ProhibitedPatternAnalyzer();
var source = "var now = DateTime.Now;";
var options = DeterminismGuardOptions.Default with
{
ExcludePatterns = ["test.cs"]
};
var result = analyzer.AnalyzeSource(source, "test.cs", options);
result.Passed.Should().BeTrue();
result.Violations.Should().BeEmpty();
}
[Fact]
public void AnalyzeSource_PassesCleanCode()
{
var analyzer = new ProhibitedPatternAnalyzer();
var source = """
public class PolicyEvaluator
{
public bool Evaluate(PolicyContext context)
{
return context.Severity.Score > 7.0m;
}
}
""";
var result = analyzer.AnalyzeSource(source, "evaluator.cs", DeterminismGuardOptions.Default);
result.Passed.Should().BeTrue();
result.Violations.Should().BeEmpty();
}
[Fact]
public void AnalyzeSource_TracksLineNumbers()
{
var analyzer = new ProhibitedPatternAnalyzer();
var source = """
public class Test
{
public void Method()
{
var now = DateTime.Now;
}
}
""";
var result = analyzer.AnalyzeSource(source, "test.cs", DeterminismGuardOptions.Default);
result.Violations.Should().ContainSingle(v => v.LineNumber == 5);
}
[Fact]
public void AnalyzeMultiple_AggregatesViolations()
{
var analyzer = new ProhibitedPatternAnalyzer();
var sources = new[]
{
("file1.cs", "var now = DateTime.Now;"),
("file2.cs", "var rng = new Random();"),
("file3.cs", "var id = Guid.NewGuid();")
};
var result = analyzer.AnalyzeMultiple(
sources.Select(s => (s.Item2, s.Item1)),
DeterminismGuardOptions.Default);
result.Violations.Should().HaveCount(3);
result.Violations.Select(v => v.SourceFile).Should()
.BeEquivalentTo(["file1.cs", "file2.cs", "file3.cs"]);
}
#endregion
#region DeterminismGuardService Tests
[Fact]
public void CreateScope_ReturnsFixedTimestamp()
{
var guard = new DeterminismGuardService();
var timestamp = new DateTimeOffset(2025, 1, 15, 12, 0, 0, TimeSpan.Zero);
using var scope = guard.CreateScope("test-scope", timestamp);
scope.GetTimestamp().Should().Be(timestamp);
scope.EvaluationTimestamp.Should().Be(timestamp);
}
[Fact]
public void CreateScope_TracksViolations()
{
var guard = new DeterminismGuardService();
using var scope = guard.CreateScope("test-scope", DateTimeOffset.UtcNow);
var violation = new DeterminismViolation
{
Category = DeterminismViolationCategory.WallClock,
ViolationType = "Test",
Message = "Test violation",
Severity = DeterminismViolationSeverity.Warning
};
scope.ReportViolation(violation);
scope.GetViolations().Should().ContainSingle(v => v.Message == "Test violation");
}
[Fact]
public void CreateScope_ThrowsOnBlockingViolationWhenEnforcementEnabled()
{
var options = new DeterminismGuardOptions
{
EnforcementEnabled = true,
FailOnSeverity = DeterminismViolationSeverity.Error
};
var guard = new DeterminismGuardService(options);
using var scope = guard.CreateScope("test-scope", DateTimeOffset.UtcNow);
var violation = new DeterminismViolation
{
Category = DeterminismViolationCategory.WallClock,
ViolationType = "Test",
Message = "Blocking violation",
Severity = DeterminismViolationSeverity.Error
};
var act = () => scope.ReportViolation(violation);
act.Should().Throw<DeterminismViolationException>()
.Which.Violation.Should().Be(violation);
}
[Fact]
public void CreateScope_DoesNotThrowWhenEnforcementDisabled()
{
var options = new DeterminismGuardOptions
{
EnforcementEnabled = false
};
var guard = new DeterminismGuardService(options);
using var scope = guard.CreateScope("test-scope", DateTimeOffset.UtcNow);
var violation = new DeterminismViolation
{
Category = DeterminismViolationCategory.WallClock,
ViolationType = "Test",
Message = "Should not throw",
Severity = DeterminismViolationSeverity.Critical
};
var act = () => scope.ReportViolation(violation);
act.Should().NotThrow();
}
[Fact]
public void Complete_ReturnsAnalysisResult()
{
var guard = new DeterminismGuardService();
using var scope = guard.CreateScope("test-scope", DateTimeOffset.UtcNow);
scope.ReportViolation(new DeterminismViolation
{
Category = DeterminismViolationCategory.RandomNumber,
ViolationType = "Test",
Message = "Warning violation",
Severity = DeterminismViolationSeverity.Warning
});
var result = scope.Complete();
result.Passed.Should().BeTrue(); // Only warnings, no errors
result.Violations.Should().HaveCount(1);
result.CountBySeverity.Should().ContainKey(DeterminismViolationSeverity.Warning);
}
#endregion
#region DeterministicTimeProvider Tests
[Fact]
public void DeterministicTimeProvider_ReturnsFixedTimestamp()
{
var fixedTime = new DateTimeOffset(2025, 6, 15, 10, 30, 0, TimeSpan.Zero);
var provider = new DeterministicTimeProvider(fixedTime);
provider.GetUtcNow().Should().Be(fixedTime);
provider.GetUtcNow().Should().Be(fixedTime); // Same value on repeated calls
}
[Fact]
public void DeterministicTimeProvider_ReturnsUtcTimeZone()
{
var provider = new DeterministicTimeProvider(DateTimeOffset.UtcNow);
provider.LocalTimeZone.Should().Be(TimeZoneInfo.Utc);
}
#endregion
#region GuardedPolicyEvaluator Tests
[Fact]
public void Evaluate_ReturnsResultWithViolations()
{
var evaluator = new GuardedPolicyEvaluator();
var timestamp = DateTimeOffset.UtcNow;
var result = evaluator.Evaluate("test-scope", timestamp, scope =>
{
scope.ReportViolation(new DeterminismViolation
{
Category = DeterminismViolationCategory.WallClock,
ViolationType = "Test",
Message = "Test warning",
Severity = DeterminismViolationSeverity.Warning
});
return 42;
});
result.Succeeded.Should().BeTrue();
result.Result.Should().Be(42);
result.HasViolations.Should().BeTrue();
result.Violations.Should().HaveCount(1);
}
[Fact]
public void Evaluate_CapturesBlockingViolation()
{
var options = new DeterminismGuardOptions
{
EnforcementEnabled = true,
FailOnSeverity = DeterminismViolationSeverity.Error
};
var evaluator = new GuardedPolicyEvaluator(options);
var result = evaluator.Evaluate("test-scope", DateTimeOffset.UtcNow, scope =>
{
scope.ReportViolation(new DeterminismViolation
{
Category = DeterminismViolationCategory.NetworkAccess,
ViolationType = "HttpClient",
Message = "Network access blocked",
Severity = DeterminismViolationSeverity.Critical
});
return "should not return";
});
result.Succeeded.Should().BeFalse();
result.WasBlocked.Should().BeTrue();
result.BlockingViolation.Should().NotBeNull();
}
[Fact]
public void ValidatePolicySource_ReturnsViolations()
{
var evaluator = new GuardedPolicyEvaluator();
var source = "var now = DateTime.Now;";
var result = evaluator.ValidatePolicySource(source, "policy.cs");
result.Violations.Should().ContainSingle();
}
[Fact]
public async Task EvaluateAsync_WorksWithAsyncCode()
{
var evaluator = new GuardedPolicyEvaluator();
var result = await evaluator.EvaluateAsync("async-scope", DateTimeOffset.UtcNow, async scope =>
{
await Task.Delay(1);
return "async result";
});
result.Succeeded.Should().BeTrue();
result.Result.Should().Be("async result");
}
#endregion
#region DeterminismGuardOptions Tests
[Fact]
public void Default_HasEnforcementEnabled()
{
DeterminismGuardOptions.Default.EnforcementEnabled.Should().BeTrue();
DeterminismGuardOptions.Default.FailOnSeverity.Should().Be(DeterminismViolationSeverity.Error);
}
[Fact]
public void Development_HasEnforcementDisabled()
{
DeterminismGuardOptions.Development.EnforcementEnabled.Should().BeFalse();
DeterminismGuardOptions.Development.FailOnSeverity.Should().Be(DeterminismViolationSeverity.Critical);
}
#endregion
}

View File

@@ -0,0 +1,319 @@
using System.Collections.Immutable;
using FluentAssertions;
using Microsoft.Extensions.Time.Testing;
using StellaOps.Policy.Engine.IncrementalOrchestrator;
using StellaOps.Policy.Engine.Telemetry;
using Xunit;
namespace StellaOps.Policy.Engine.Tests.IncrementalOrchestrator;
public sealed class IncrementalOrchestratorTests
{
#region PolicyChangeEvent Tests
[Fact]
public void CreateAdvisoryUpdated_CreatesValidEvent()
{
var now = DateTimeOffset.UtcNow;
var evt = PolicyChangeEventFactory.CreateAdvisoryUpdated(
tenantId: "test-tenant",
advisoryId: "GHSA-test-001",
vulnerabilityId: "CVE-2021-12345",
affectedPurls: ["pkg:npm/lodash", "pkg:npm/express"],
source: "concelier",
occurredAt: now,
createdAt: now);
evt.ChangeType.Should().Be(PolicyChangeType.AdvisoryUpdated);
evt.TenantId.Should().Be("test-tenant");
evt.AdvisoryId.Should().Be("GHSA-test-001");
evt.VulnerabilityId.Should().Be("CVE-2021-12345");
evt.AffectedPurls.Should().HaveCount(2);
evt.EventId.Should().StartWith("pce-");
evt.ContentHash.Should().NotBeNullOrEmpty();
}
[Fact]
public void CreateVexUpdated_CreatesValidEvent()
{
var now = DateTimeOffset.UtcNow;
var evt = PolicyChangeEventFactory.CreateVexUpdated(
tenantId: "test-tenant",
vulnerabilityId: "CVE-2021-12345",
affectedProductKeys: ["pkg:npm/lodash"],
source: "excititor",
occurredAt: now,
createdAt: now);
evt.ChangeType.Should().Be(PolicyChangeType.VexStatementUpdated);
evt.VulnerabilityId.Should().Be("CVE-2021-12345");
evt.AffectedProductKeys.Should().ContainSingle();
}
[Fact]
public void CreateSbomUpdated_CreatesValidEvent()
{
var now = DateTimeOffset.UtcNow;
var evt = PolicyChangeEventFactory.CreateSbomUpdated(
tenantId: "test-tenant",
sbomId: "sbom-123",
productKey: "myapp:v1.0.0",
componentPurls: ["pkg:npm/lodash@4.17.21"],
source: "scanner",
occurredAt: now,
createdAt: now);
evt.ChangeType.Should().Be(PolicyChangeType.SbomUpdated);
evt.AffectedSbomIds.Should().Contain("sbom-123");
evt.AffectedProductKeys.Should().Contain("myapp:v1.0.0");
}
[Fact]
public void ComputeContentHash_IsDeterministic()
{
var hash1 = PolicyChangeEvent.ComputeContentHash(
PolicyChangeType.AdvisoryUpdated,
"tenant",
"ADV-001",
"CVE-001",
["pkg:npm/a", "pkg:npm/b"],
null,
null);
var hash2 = PolicyChangeEvent.ComputeContentHash(
PolicyChangeType.AdvisoryUpdated,
"tenant",
"ADV-001",
"CVE-001",
["pkg:npm/b", "pkg:npm/a"], // Different order
null,
null);
hash1.Should().Be(hash2); // Should be equal due to sorting
}
[Fact]
public void ComputeContentHash_DiffersForDifferentInput()
{
var hash1 = PolicyChangeEvent.ComputeContentHash(
PolicyChangeType.AdvisoryUpdated,
"tenant",
"ADV-001",
"CVE-001",
null, null, null);
var hash2 = PolicyChangeEvent.ComputeContentHash(
PolicyChangeType.AdvisoryUpdated,
"tenant",
"ADV-002", // Different advisory
"CVE-001",
null, null, null);
hash1.Should().NotBe(hash2);
}
[Fact]
public void CreateManualTrigger_IncludesRequestedBy()
{
var now = DateTimeOffset.UtcNow;
var evt = PolicyChangeEventFactory.CreateManualTrigger(
tenantId: "test-tenant",
policyIds: ["policy-1"],
sbomIds: ["sbom-1"],
productKeys: null,
requestedBy: "admin@example.com",
createdAt: now);
evt.ChangeType.Should().Be(PolicyChangeType.ManualTrigger);
evt.Metadata.Should().ContainKey("requestedBy");
evt.Metadata["requestedBy"].Should().Be("admin@example.com");
}
#endregion
#region IncrementalPolicyOrchestrator Tests
[Fact]
public async Task ProcessAsync_ProcessesEvents()
{
var eventSource = new InMemoryPolicyChangeEventSource();
var submitter = new TestSubmitter();
var idempotencyStore = new InMemoryPolicyChangeIdempotencyStore();
var timeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow);
var orchestrator = new IncrementalPolicyOrchestrator(
eventSource, submitter, idempotencyStore,
timeProvider: timeProvider);
eventSource.Enqueue(PolicyChangeEventFactory.CreateAdvisoryUpdated(
"tenant1", "ADV-001", "CVE-001", ["pkg:npm/test"],
"test", timeProvider.GetUtcNow(), timeProvider.GetUtcNow()));
var result = await orchestrator.ProcessAsync(CancellationToken.None);
result.TotalEventsRead.Should().Be(1);
result.BatchesProcessed.Should().Be(1);
submitter.SubmittedBatches.Should().HaveCount(1);
}
[Fact]
public async Task ProcessAsync_DeduplicatesEvents()
{
var eventSource = new InMemoryPolicyChangeEventSource();
var submitter = new TestSubmitter();
var idempotencyStore = new InMemoryPolicyChangeIdempotencyStore();
var timeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow);
var orchestrator = new IncrementalPolicyOrchestrator(
eventSource, submitter, idempotencyStore,
timeProvider: timeProvider);
var evt = PolicyChangeEventFactory.CreateAdvisoryUpdated(
"tenant1", "ADV-001", "CVE-001", ["pkg:npm/test"],
"test", timeProvider.GetUtcNow(), timeProvider.GetUtcNow());
// Mark as already seen
await idempotencyStore.MarkSeenAsync(evt.EventId, timeProvider.GetUtcNow(), CancellationToken.None);
eventSource.Enqueue(evt);
var result = await orchestrator.ProcessAsync(CancellationToken.None);
result.TotalEventsRead.Should().Be(1);
result.EventsSkippedDuplicate.Should().Be(1);
result.BatchesProcessed.Should().Be(0);
}
[Fact]
public async Task ProcessAsync_SkipsOldEvents()
{
var options = new IncrementalOrchestratorOptions
{
MaxEventAge = TimeSpan.FromHours(1)
};
var eventSource = new InMemoryPolicyChangeEventSource();
var submitter = new TestSubmitter();
var idempotencyStore = new InMemoryPolicyChangeIdempotencyStore();
var timeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow);
var orchestrator = new IncrementalPolicyOrchestrator(
eventSource, submitter, idempotencyStore, options,
timeProvider: timeProvider);
// Create an old event
var oldTime = timeProvider.GetUtcNow().AddHours(-2);
var evt = PolicyChangeEventFactory.CreateAdvisoryUpdated(
"tenant1", "ADV-001", "CVE-001", ["pkg:npm/test"],
"test", oldTime, oldTime);
eventSource.Enqueue(evt);
var result = await orchestrator.ProcessAsync(CancellationToken.None);
result.TotalEventsRead.Should().Be(1);
result.EventsSkippedOld.Should().Be(1);
result.BatchesProcessed.Should().Be(0);
}
[Fact]
public async Task ProcessAsync_GroupsByTenant()
{
var eventSource = new InMemoryPolicyChangeEventSource();
var submitter = new TestSubmitter();
var idempotencyStore = new InMemoryPolicyChangeIdempotencyStore();
var timeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow);
var orchestrator = new IncrementalPolicyOrchestrator(
eventSource, submitter, idempotencyStore,
timeProvider: timeProvider);
eventSource.Enqueue(PolicyChangeEventFactory.CreateAdvisoryUpdated(
"tenant1", "ADV-001", "CVE-001", [], "test",
timeProvider.GetUtcNow(), timeProvider.GetUtcNow()));
eventSource.Enqueue(PolicyChangeEventFactory.CreateAdvisoryUpdated(
"tenant2", "ADV-002", "CVE-002", [], "test",
timeProvider.GetUtcNow(), timeProvider.GetUtcNow()));
var result = await orchestrator.ProcessAsync(CancellationToken.None);
result.BatchesProcessed.Should().Be(2); // One per tenant
submitter.SubmittedBatches.Select(b => b.TenantId).Should()
.BeEquivalentTo(["tenant1", "tenant2"]);
}
[Fact]
public async Task ProcessAsync_SortsByPriority()
{
var eventSource = new InMemoryPolicyChangeEventSource();
var submitter = new TestSubmitter();
var idempotencyStore = new InMemoryPolicyChangeIdempotencyStore();
var timeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow);
var orchestrator = new IncrementalPolicyOrchestrator(
eventSource, submitter, idempotencyStore,
timeProvider: timeProvider);
// Add normal priority first
eventSource.Enqueue(PolicyChangeEventFactory.CreateAdvisoryUpdated(
"tenant1", "ADV-001", "CVE-001", [], "test",
timeProvider.GetUtcNow(), timeProvider.GetUtcNow(),
priority: PolicyChangePriority.Normal));
// Add emergency priority second
eventSource.Enqueue(PolicyChangeEventFactory.CreateAdvisoryUpdated(
"tenant1", "ADV-002", "CVE-002", [], "test",
timeProvider.GetUtcNow(), timeProvider.GetUtcNow(),
priority: PolicyChangePriority.Emergency));
await orchestrator.ProcessAsync(CancellationToken.None);
// Emergency should be processed first (separate batch due to priority)
submitter.SubmittedBatches.Should().HaveCount(2);
submitter.SubmittedBatches[0].Priority.Should().Be(PolicyChangePriority.Emergency);
}
#endregion
#region RuleHitSamplingOptions Tests
[Fact]
public void Default_HasReasonableSamplingRates()
{
var options = RuleHitSamplingOptions.Default;
options.BaseSamplingRate.Should().BeInRange(0.0, 1.0);
options.VexOverrideSamplingRate.Should().Be(1.0); // Always sample VEX
options.IncidentModeSamplingRate.Should().Be(1.0);
}
[Fact]
public void FullSampling_SamplesEverything()
{
var options = RuleHitSamplingOptions.FullSampling;
options.BaseSamplingRate.Should().Be(1.0);
options.VexOverrideSamplingRate.Should().Be(1.0);
options.HighSeveritySamplingRate.Should().Be(1.0);
}
#endregion
private sealed class TestSubmitter : IPolicyReEvaluationSubmitter
{
public List<PolicyChangeBatch> SubmittedBatches { get; } = [];
public Task<PolicyReEvaluationResult> SubmitAsync(
PolicyChangeBatch batch,
CancellationToken cancellationToken)
{
SubmittedBatches.Add(batch);
return Task.FromResult(new PolicyReEvaluationResult
{
Succeeded = true,
JobIds = [$"job-{batch.BatchId}"],
ProcessingTimeMs = 1
});
}
}
}

View File

@@ -0,0 +1,268 @@
using System.Collections.Immutable;
using FluentAssertions;
using StellaOps.Policy.Engine.Materialization;
using Xunit;
namespace StellaOps.Policy.Engine.Tests.Materialization;
public sealed class MaterializationTests
{
#region EffectiveFinding.CreateId Tests
[Fact]
public void CreateId_IsDeterministic()
{
var id1 = EffectiveFinding.CreateId("tenant1", "policy-1", "pkg:npm/lodash@4.17.21", "CVE-2021-12345");
var id2 = EffectiveFinding.CreateId("tenant1", "policy-1", "pkg:npm/lodash@4.17.21", "CVE-2021-12345");
id1.Should().Be(id2);
id1.Should().StartWith("sha256:");
}
[Fact]
public void CreateId_NormalizesTenant()
{
var id1 = EffectiveFinding.CreateId("TENANT1", "policy-1", "pkg:npm/lodash", "CVE-2021-12345");
var id2 = EffectiveFinding.CreateId("tenant1", "policy-1", "pkg:npm/lodash", "CVE-2021-12345");
id1.Should().Be(id2);
}
[Fact]
public void CreateId_NormalizesPurl()
{
var id1 = EffectiveFinding.CreateId("tenant1", "policy-1", "PKG:NPM/LODASH", "CVE-2021-12345");
var id2 = EffectiveFinding.CreateId("tenant1", "policy-1", "pkg:npm/lodash", "CVE-2021-12345");
id1.Should().Be(id2);
}
[Fact]
public void CreateId_DiffersForDifferentInput()
{
var id1 = EffectiveFinding.CreateId("tenant1", "policy-1", "pkg:npm/lodash", "CVE-2021-12345");
var id2 = EffectiveFinding.CreateId("tenant1", "policy-1", "pkg:npm/lodash", "CVE-2021-99999");
id1.Should().NotBe(id2);
}
[Fact]
public void CreateId_HandlesNullValues()
{
var id = EffectiveFinding.CreateId(null!, "policy", "purl", "advisory");
id.Should().StartWith("sha256:");
}
#endregion
#region EffectiveFinding.ComputeContentHash Tests
[Fact]
public void ComputeContentHash_IsDeterministic()
{
var hash1 = EffectiveFinding.ComputeContentHash("affected", "High", "severity-rule", "not_affected", null);
var hash2 = EffectiveFinding.ComputeContentHash("affected", "High", "severity-rule", "not_affected", null);
hash1.Should().Be(hash2);
}
[Fact]
public void ComputeContentHash_DiffersForDifferentStatus()
{
var hash1 = EffectiveFinding.ComputeContentHash("affected", "High", null, null, null);
var hash2 = EffectiveFinding.ComputeContentHash("suppressed", "High", null, null, null);
hash1.Should().NotBe(hash2);
}
[Fact]
public void ComputeContentHash_DiffersForDifferentSeverity()
{
var hash1 = EffectiveFinding.ComputeContentHash("affected", "High", null, null, null);
var hash2 = EffectiveFinding.ComputeContentHash("affected", "Critical", null, null, null);
hash1.Should().NotBe(hash2);
}
[Fact]
public void ComputeContentHash_IncludesAnnotations()
{
var annotations = new Dictionary<string, string> { ["key"] = "value" };
var hash1 = EffectiveFinding.ComputeContentHash("affected", "High", null, null, annotations);
var hash2 = EffectiveFinding.ComputeContentHash("affected", "High", null, null, null);
hash1.Should().NotBe(hash2);
}
[Fact]
public void ComputeContentHash_SortsAnnotationsDeterministically()
{
var annotations1 = new Dictionary<string, string> { ["a"] = "1", ["b"] = "2" };
var annotations2 = new Dictionary<string, string> { ["b"] = "2", ["a"] = "1" };
var hash1 = EffectiveFinding.ComputeContentHash("affected", null, null, null, annotations1);
var hash2 = EffectiveFinding.ComputeContentHash("affected", null, null, null, annotations2);
hash1.Should().Be(hash2);
}
#endregion
#region EffectiveFindingHistoryEntry Tests
[Fact]
public void HistoryEntry_CreateId_IsDeterministic()
{
var id1 = EffectiveFindingHistoryEntry.CreateId("finding-1", 5);
var id2 = EffectiveFindingHistoryEntry.CreateId("finding-1", 5);
id1.Should().Be(id2);
id1.Should().Be("finding-1:v5");
}
[Fact]
public void HistoryEntry_CreateId_DiffersForDifferentVersion()
{
var id1 = EffectiveFindingHistoryEntry.CreateId("finding-1", 1);
var id2 = EffectiveFindingHistoryEntry.CreateId("finding-1", 2);
id1.Should().NotBe(id2);
}
#endregion
#region MaterializeFindingInput Tests
[Fact]
public void MaterializeFindingInput_CanBeCreated()
{
var input = new MaterializeFindingInput
{
TenantId = "tenant-1",
PolicyId = "policy-1",
PolicyVersion = 1,
ComponentPurl = "pkg:npm/lodash@4.17.21",
ComponentName = "lodash",
ComponentVersion = "4.17.21",
AdvisoryId = "CVE-2021-12345",
AdvisorySource = "nvd",
Status = "affected",
Severity = "High",
RuleName = "severity-rule",
VexStatus = "not_affected",
VexJustification = "vulnerable_code_not_in_execute_path",
Annotations = ImmutableDictionary<string, string>.Empty.Add("key", "value"),
PolicyRunId = "run-123",
TraceId = "trace-abc",
SpanId = "span-def"
};
input.TenantId.Should().Be("tenant-1");
input.PolicyId.Should().Be("policy-1");
input.PolicyVersion.Should().Be(1);
input.ComponentPurl.Should().Be("pkg:npm/lodash@4.17.21");
input.Status.Should().Be("affected");
input.VexStatus.Should().Be("not_affected");
}
#endregion
#region MaterializeFindingResult Tests
[Fact]
public void MaterializeFindingResult_TracksCreation()
{
var result = new MaterializeFindingResult
{
FindingId = "sha256:abc123",
WasCreated = true,
WasUpdated = false,
HistoryVersion = 1,
ChangeType = EffectiveFindingChangeType.Created
};
result.WasCreated.Should().BeTrue();
result.WasUpdated.Should().BeFalse();
result.ChangeType.Should().Be(EffectiveFindingChangeType.Created);
}
[Fact]
public void MaterializeFindingResult_TracksUpdate()
{
var result = new MaterializeFindingResult
{
FindingId = "sha256:abc123",
WasCreated = false,
WasUpdated = true,
HistoryVersion = 2,
ChangeType = EffectiveFindingChangeType.StatusChanged
};
result.WasCreated.Should().BeFalse();
result.WasUpdated.Should().BeTrue();
result.ChangeType.Should().Be(EffectiveFindingChangeType.StatusChanged);
}
#endregion
#region MaterializeBatchResult Tests
[Fact]
public void MaterializeBatchResult_AggregatesCorrectly()
{
var results = ImmutableArray.Create(
new MaterializeFindingResult
{
FindingId = "id1",
WasCreated = true,
WasUpdated = false,
HistoryVersion = 1,
ChangeType = EffectiveFindingChangeType.Created
},
new MaterializeFindingResult
{
FindingId = "id2",
WasCreated = false,
WasUpdated = true,
HistoryVersion = 2,
ChangeType = EffectiveFindingChangeType.StatusChanged
}
);
var batchResult = new MaterializeBatchResult
{
TotalInputs = 3,
Created = 1,
Updated = 1,
Unchanged = 1,
Errors = 0,
ProcessingTimeMs = 100,
Results = results
};
batchResult.TotalInputs.Should().Be(3);
batchResult.Created.Should().Be(1);
batchResult.Updated.Should().Be(1);
batchResult.Unchanged.Should().Be(1);
batchResult.Results.Should().HaveCount(2);
}
#endregion
#region EffectiveFindingChangeType Tests
[Theory]
[InlineData(EffectiveFindingChangeType.Created, "Created")]
[InlineData(EffectiveFindingChangeType.StatusChanged, "StatusChanged")]
[InlineData(EffectiveFindingChangeType.SeverityChanged, "SeverityChanged")]
[InlineData(EffectiveFindingChangeType.VexApplied, "VexApplied")]
[InlineData(EffectiveFindingChangeType.AnnotationsChanged, "AnnotationsChanged")]
[InlineData(EffectiveFindingChangeType.PolicyVersionChanged, "PolicyVersionChanged")]
public void EffectiveFindingChangeType_HasExpectedValues(EffectiveFindingChangeType changeType, string expectedName)
{
changeType.ToString().Should().Be(expectedName);
}
#endregion
}

View File

@@ -128,7 +128,8 @@ public sealed class PolicyBundleServiceTests
var compiler = new PolicyCompiler();
var complexity = new PolicyComplexityAnalyzer();
var options = Microsoft.Extensions.Options.Options.Create(new PolicyEngineOptions());
var compilationService = new PolicyCompilationService(compiler, complexity, new StaticOptionsMonitor(options.Value), TimeProvider.System);
var metadataExtractor = new PolicyMetadataExtractor();
var compilationService = new PolicyCompilationService(compiler, complexity, metadataExtractor, new StaticOptionsMonitor(options.Value), TimeProvider.System);
var repo = new InMemoryPolicyPackRepository();
return new ServiceHarness(
new PolicyBundleService(compilationService, repo, TimeProvider.System),

View File

@@ -84,7 +84,8 @@ public sealed class PolicyCompilationServiceTests
options.Compilation.MaxDurationMilliseconds = maxDurationMilliseconds;
var optionsMonitor = new StaticOptionsMonitor<PolicyEngineOptions>(options);
var timeProvider = new FakeTimeProvider(simulatedDurationMilliseconds);
return new PolicyCompilationService(compiler, analyzer, optionsMonitor, timeProvider);
var metadataExtractor = new PolicyMetadataExtractor();
return new PolicyCompilationService(compiler, analyzer, metadataExtractor, optionsMonitor, timeProvider);
}
private sealed class StaticOptionsMonitor<T> : IOptionsMonitor<T>

View File

@@ -157,8 +157,8 @@ public sealed class PolicyRuntimeEvaluationServiceTests
var responses = await harness.Service.EvaluateBatchAsync(requests, CancellationToken.None);
Assert.Equal(2, responses.Count);
Assert.True(responses.Any(r => r.Cached));
Assert.True(responses.Any(r => !r.Cached));
Assert.Contains(responses, r => r.Cached);
Assert.Contains(responses, r => !r.Cached);
}
[Fact]
@@ -231,7 +231,8 @@ public sealed class PolicyRuntimeEvaluationServiceTests
var analyzer = new PolicyComplexityAnalyzer();
var options = new PolicyEngineOptions();
var optionsMonitor = new StaticOptionsMonitor(options);
return new PolicyCompilationService(compiler, analyzer, optionsMonitor, TimeProvider.System);
var metadataExtractor = new PolicyMetadataExtractor();
return new PolicyCompilationService(compiler, analyzer, metadataExtractor, optionsMonitor, TimeProvider.System);
}
private sealed record TestHarness(

View File

@@ -0,0 +1,380 @@
using System.Collections.Immutable;
using FluentAssertions;
using StellaOps.Policy.Engine.SelectionJoin;
using Xunit;
namespace StellaOps.Policy.Engine.Tests.SelectionJoin;
public sealed class SelectionJoinTests
{
#region PurlEquivalence Tests
[Theory]
[InlineData("pkg:npm/lodash@4.17.21", "pkg:npm/lodash")]
[InlineData("pkg:maven/org.apache.commons/commons-lang3@3.12.0", "pkg:maven/org.apache.commons/commons-lang3")]
[InlineData("pkg:pypi/requests@2.28.0", "pkg:pypi/requests")]
[InlineData("pkg:gem/rails@7.0.0", "pkg:gem/rails")]
[InlineData("pkg:nuget/Newtonsoft.Json@13.0.1", "pkg:nuget/Newtonsoft.Json")]
public void ExtractPackageKey_RemovesVersion(string purl, string expectedKey)
{
var key = PurlEquivalence.ExtractPackageKey(purl);
key.Should().Be(expectedKey);
}
[Fact]
public void ExtractPackageKey_HandlesNoVersion()
{
var purl = "pkg:npm/lodash";
var key = PurlEquivalence.ExtractPackageKey(purl);
key.Should().Be("pkg:npm/lodash");
}
[Fact]
public void ExtractPackageKey_HandlesScopedPackages()
{
var purl = "pkg:npm/@scope/package@1.0.0";
var key = PurlEquivalence.ExtractPackageKey(purl);
key.Should().Be("pkg:npm/@scope/package");
}
[Theory]
[InlineData("pkg:npm/lodash@4.17.21", "npm")]
[InlineData("pkg:maven/org.apache/commons@1.0", "maven")]
[InlineData("pkg:pypi/requests@2.28", "pypi")]
public void ExtractEcosystem_ReturnsCorrectEcosystem(string purl, string expected)
{
var ecosystem = PurlEquivalence.ExtractEcosystem(purl);
ecosystem.Should().Be(expected);
}
[Fact]
public void ComputeMatchConfidence_ExactMatch_Returns1()
{
var confidence = PurlEquivalence.ComputeMatchConfidence(
"pkg:npm/lodash@4.17.21",
"pkg:npm/lodash@4.17.21");
confidence.Should().Be(1.0);
}
[Fact]
public void ComputeMatchConfidence_PackageKeyMatch_Returns08()
{
var confidence = PurlEquivalence.ComputeMatchConfidence(
"pkg:npm/lodash@4.17.21",
"pkg:npm/lodash@4.17.20");
confidence.Should().Be(0.8);
}
#endregion
#region PurlEquivalenceTable Tests
[Fact]
public void FromGroups_CreatesEquivalentMappings()
{
var groups = new[]
{
new[] { "pkg:npm/lodash", "pkg:npm/lodash-es" }
};
var table = PurlEquivalenceTable.FromGroups(groups);
table.AreEquivalent("pkg:npm/lodash", "pkg:npm/lodash-es").Should().BeTrue();
table.GroupCount.Should().Be(1);
}
[Fact]
public void GetCanonical_ReturnsFirstLexicographically()
{
var groups = new[]
{
new[] { "pkg:npm/b-package", "pkg:npm/a-package" }
};
var table = PurlEquivalenceTable.FromGroups(groups);
// "a-package" is lexicographically first
table.GetCanonical("pkg:npm/b-package").Should().Be("pkg:npm/a-package");
}
[Fact]
public void GetEquivalents_ReturnsAllEquivalentPurls()
{
var groups = new[]
{
new[] { "pkg:npm/a", "pkg:npm/b", "pkg:npm/c" }
};
var table = PurlEquivalenceTable.FromGroups(groups);
var equivalents = table.GetEquivalents("pkg:npm/b");
equivalents.Should().HaveCount(3);
equivalents.Should().Contain("pkg:npm/a");
equivalents.Should().Contain("pkg:npm/b");
equivalents.Should().Contain("pkg:npm/c");
}
[Fact]
public void Empty_HasNoMappings()
{
var table = PurlEquivalenceTable.Empty;
table.GroupCount.Should().Be(0);
table.TotalEntries.Should().Be(0);
table.AreEquivalent("pkg:npm/a", "pkg:npm/b").Should().BeFalse();
}
#endregion
#region SelectionJoinService Tests
[Fact]
public void ResolveTuples_MatchesByExactPurl()
{
var service = new SelectionJoinService();
var input = new SelectionJoinBatchInput(
TenantId: "test-tenant",
BatchId: "batch-1",
Components: [
new SbomComponentInput(
Purl: "pkg:npm/lodash@4.17.21",
Name: "lodash",
Version: "4.17.21",
Ecosystem: "npm",
Metadata: ImmutableDictionary<string, string>.Empty)
],
Advisories: [
new AdvisoryLinksetInput(
AdvisoryId: "GHSA-test-001",
Source: "github",
Purls: ["pkg:npm/lodash@4.17.21"],
Cpes: ImmutableArray<string>.Empty,
Aliases: ["CVE-2021-12345"],
Confidence: 1.0)
],
VexLinksets: ImmutableArray<VexLinksetInput>.Empty,
EquivalenceTable: null,
Options: new SelectionJoinOptions());
var result = service.ResolveTuples(input);
result.Tuples.Should().ContainSingle();
result.Tuples[0].MatchType.Should().Be(SelectionMatchType.ExactPurl);
result.Tuples[0].Component.Purl.Should().Be("pkg:npm/lodash@4.17.21");
result.Statistics.ExactPurlMatches.Should().Be(1);
}
[Fact]
public void ResolveTuples_MatchesByPackageKey()
{
var service = new SelectionJoinService();
var input = new SelectionJoinBatchInput(
TenantId: "test-tenant",
BatchId: "batch-1",
Components: [
new SbomComponentInput("pkg:npm/lodash@4.17.21", "lodash", "4.17.21", "npm",
ImmutableDictionary<string, string>.Empty)
],
Advisories: [
new AdvisoryLinksetInput("GHSA-test-001", "github",
Purls: ["pkg:npm/lodash@4.17.20"], // Different version
Cpes: ImmutableArray<string>.Empty,
Aliases: ["CVE-2021-12345"],
Confidence: 1.0)
],
VexLinksets: ImmutableArray<VexLinksetInput>.Empty,
EquivalenceTable: null,
Options: new SelectionJoinOptions());
var result = service.ResolveTuples(input);
result.Tuples.Should().ContainSingle();
result.Tuples[0].MatchType.Should().Be(SelectionMatchType.PackageKeyMatch);
}
[Fact]
public void ResolveTuples_AppliesVexOverlay()
{
var service = new SelectionJoinService();
var input = new SelectionJoinBatchInput(
TenantId: "test-tenant",
BatchId: "batch-1",
Components: [
new SbomComponentInput("pkg:npm/lodash@4.17.21", "lodash", "4.17.21", "npm",
ImmutableDictionary<string, string>.Empty)
],
Advisories: [
new AdvisoryLinksetInput("GHSA-test-001", "github",
Purls: ["pkg:npm/lodash@4.17.21"],
Cpes: ImmutableArray<string>.Empty,
Aliases: ["CVE-2021-12345"],
Confidence: 1.0)
],
VexLinksets: [
new VexLinksetInput("vex-1", "CVE-2021-12345", "pkg:npm/lodash@4.17.21",
"not_affected", "vulnerable_code_not_in_execute_path", VexConfidenceLevel.High)
],
EquivalenceTable: null,
Options: new SelectionJoinOptions());
var result = service.ResolveTuples(input);
result.Tuples.Should().ContainSingle();
result.Tuples[0].Vex.Should().NotBeNull();
result.Tuples[0].Vex!.Status.Should().Be("not_affected");
result.Statistics.VexOverlays.Should().Be(1);
}
[Fact]
public void ResolveTuples_ProducesDeterministicOrdering()
{
var service = new SelectionJoinService();
var input = new SelectionJoinBatchInput(
TenantId: "test-tenant",
BatchId: "batch-1",
Components: [
new SbomComponentInput("pkg:npm/z-package@1.0.0", "z", "1.0.0", "npm",
ImmutableDictionary<string, string>.Empty),
new SbomComponentInput("pkg:npm/a-package@1.0.0", "a", "1.0.0", "npm",
ImmutableDictionary<string, string>.Empty),
new SbomComponentInput("pkg:npm/m-package@1.0.0", "m", "1.0.0", "npm",
ImmutableDictionary<string, string>.Empty)
],
Advisories: [
new AdvisoryLinksetInput("ADV-001", "test",
Purls: ["pkg:npm/z-package", "pkg:npm/a-package", "pkg:npm/m-package"],
Cpes: ImmutableArray<string>.Empty,
Aliases: ["CVE-2021-001"],
Confidence: 1.0)
],
VexLinksets: ImmutableArray<VexLinksetInput>.Empty,
EquivalenceTable: null,
Options: new SelectionJoinOptions());
var result = service.ResolveTuples(input);
// Should be sorted by component PURL
result.Tuples.Should().HaveCount(3);
result.Tuples[0].Component.Purl.Should().Be("pkg:npm/a-package@1.0.0");
result.Tuples[1].Component.Purl.Should().Be("pkg:npm/m-package@1.0.0");
result.Tuples[2].Component.Purl.Should().Be("pkg:npm/z-package@1.0.0");
}
[Fact]
public void ResolveTuples_HandlesMultipleAdvisories()
{
var service = new SelectionJoinService();
var input = new SelectionJoinBatchInput(
TenantId: "test-tenant",
BatchId: "batch-1",
Components: [
new SbomComponentInput("pkg:npm/lodash@4.17.21", "lodash", "4.17.21", "npm",
ImmutableDictionary<string, string>.Empty)
],
Advisories: [
new AdvisoryLinksetInput("ADV-001", "test",
Purls: ["pkg:npm/lodash@4.17.21"],
Cpes: ImmutableArray<string>.Empty,
Aliases: ["CVE-2021-001"],
Confidence: 1.0),
new AdvisoryLinksetInput("ADV-002", "test",
Purls: ["pkg:npm/lodash@4.17.21"],
Cpes: ImmutableArray<string>.Empty,
Aliases: ["CVE-2021-002"],
Confidence: 1.0)
],
VexLinksets: ImmutableArray<VexLinksetInput>.Empty,
EquivalenceTable: null,
Options: new SelectionJoinOptions());
var result = service.ResolveTuples(input);
result.Tuples.Should().HaveCount(2);
result.Tuples.Should().Contain(t => t.Advisory.AdvisoryId == "ADV-001");
result.Tuples.Should().Contain(t => t.Advisory.AdvisoryId == "ADV-002");
}
[Fact]
public void ResolveTuples_ReturnsStatistics()
{
var service = new SelectionJoinService();
var input = new SelectionJoinBatchInput(
TenantId: "test-tenant",
BatchId: "batch-1",
Components: [
new SbomComponentInput("pkg:npm/a@1.0.0", "a", "1.0.0", "npm",
ImmutableDictionary<string, string>.Empty),
new SbomComponentInput("pkg:npm/b@1.0.0", "b", "1.0.0", "npm",
ImmutableDictionary<string, string>.Empty)
],
Advisories: [
new AdvisoryLinksetInput("ADV-001", "test",
Purls: ["pkg:npm/a"],
Cpes: ImmutableArray<string>.Empty,
Aliases: ["CVE-001"],
Confidence: 1.0)
],
VexLinksets: ImmutableArray<VexLinksetInput>.Empty,
EquivalenceTable: null,
Options: new SelectionJoinOptions());
var result = service.ResolveTuples(input);
result.Statistics.TotalComponents.Should().Be(2);
result.Statistics.TotalAdvisories.Should().Be(1);
result.Statistics.MatchedTuples.Should().Be(1);
result.UnmatchedComponents.Should().ContainSingle(c => c.Purl == "pkg:npm/b@1.0.0");
}
[Fact]
public void ResolveTuples_HandlesEmptyInput()
{
var service = new SelectionJoinService();
var input = new SelectionJoinBatchInput(
TenantId: "test-tenant",
BatchId: "batch-1",
Components: ImmutableArray<SbomComponentInput>.Empty,
Advisories: ImmutableArray<AdvisoryLinksetInput>.Empty,
VexLinksets: ImmutableArray<VexLinksetInput>.Empty,
EquivalenceTable: null,
Options: new SelectionJoinOptions());
var result = service.ResolveTuples(input);
result.Tuples.Should().BeEmpty();
result.Statistics.TotalComponents.Should().Be(0);
}
#endregion
#region SelectionJoinTuple Tests
[Fact]
public void CreateTupleId_IsDeterministic()
{
var id1 = SelectionJoinTuple.CreateTupleId("tenant1", "pkg:npm/lodash@4.17.21", "CVE-2021-12345");
var id2 = SelectionJoinTuple.CreateTupleId("tenant1", "pkg:npm/lodash@4.17.21", "CVE-2021-12345");
id1.Should().Be(id2);
id1.Should().StartWith("tuple:sha256:");
}
[Fact]
public void CreateTupleId_NormalizesInput()
{
var id1 = SelectionJoinTuple.CreateTupleId("TENANT1", "PKG:NPM/LODASH@4.17.21", "CVE-2021-12345");
var id2 = SelectionJoinTuple.CreateTupleId("tenant1", "pkg:npm/lodash@4.17.21", "CVE-2021-12345");
id1.Should().Be(id2);
}
#endregion
}

View File

@@ -0,0 +1,414 @@
using System.Collections.Immutable;
using FluentAssertions;
using StellaOps.Policy.Engine.Simulation;
using StellaOps.Policy.Engine.Telemetry;
using Xunit;
namespace StellaOps.Policy.Engine.Tests.Simulation;
public sealed class SimulationAnalyticsServiceTests
{
private readonly SimulationAnalyticsService _service = new();
[Fact]
public void ComputeRuleFiringCounts_EmptyTraces_ReturnsEmptyCounts()
{
// Arrange
var traces = Array.Empty<RuleHitTrace>();
// Act
var result = _service.ComputeRuleFiringCounts(traces, 10);
// Assert
result.TotalEvaluations.Should().Be(10);
result.TotalRulesFired.Should().Be(0);
result.RulesByName.Should().BeEmpty();
result.RulesByPriority.Should().BeEmpty();
result.RulesByOutcome.Should().BeEmpty();
result.TopRules.Should().BeEmpty();
}
[Fact]
public void ComputeRuleFiringCounts_WithFiredRules_CountsCorrectly()
{
// Arrange
var traces = new[]
{
CreateTrace("rule_a", 1, "block", expressionResult: true),
CreateTrace("rule_a", 1, "block", expressionResult: true),
CreateTrace("rule_b", 2, "allow", expressionResult: true),
CreateTrace("rule_c", 3, "warn", expressionResult: false), // Not fired
};
// Act
var result = _service.ComputeRuleFiringCounts(traces, 10);
// Assert
result.TotalRulesFired.Should().Be(3);
result.RulesByName.Should().HaveCount(2);
result.RulesByName["rule_a"].FireCount.Should().Be(2);
result.RulesByName["rule_b"].FireCount.Should().Be(1);
result.RulesByPriority[1].Should().Be(2);
result.RulesByPriority[2].Should().Be(1);
result.RulesByOutcome["block"].Should().Be(2);
result.RulesByOutcome["allow"].Should().Be(1);
}
[Fact]
public void ComputeRuleFiringCounts_TopRules_OrderedByFireCount()
{
// Arrange
var traces = new List<RuleHitTrace>();
for (var i = 0; i < 15; i++)
{
traces.Add(CreateTrace("frequently_fired", 1, "block", expressionResult: true));
}
for (var i = 0; i < 5; i++)
{
traces.Add(CreateTrace("sometimes_fired", 2, "warn", expressionResult: true));
}
traces.Add(CreateTrace("rarely_fired", 3, "allow", expressionResult: true));
// Act
var result = _service.ComputeRuleFiringCounts(traces, 100);
// Assert
result.TopRules.Should().HaveCount(3);
result.TopRules[0].RuleName.Should().Be("frequently_fired");
result.TopRules[0].FireCount.Should().Be(15);
result.TopRules[1].RuleName.Should().Be("sometimes_fired");
result.TopRules[1].FireCount.Should().Be(5);
result.TopRules[2].RuleName.Should().Be("rarely_fired");
result.TopRules[2].FireCount.Should().Be(1);
}
[Fact]
public void ComputeRuleFiringCounts_VexOverrides_CountedCorrectly()
{
// Arrange
var traces = new[]
{
CreateTrace("rule_a", 1, "allow", expressionResult: true, isVexOverride: true, vexVendor: "vendor_a", vexStatus: "not_affected"),
CreateTrace("rule_a", 1, "allow", expressionResult: true, isVexOverride: true, vexVendor: "vendor_a", vexStatus: "fixed"),
CreateTrace("rule_b", 2, "allow", expressionResult: true, isVexOverride: true, vexVendor: "vendor_b", vexStatus: "not_affected"),
CreateTrace("rule_c", 3, "block", expressionResult: true),
};
// Act
var result = _service.ComputeRuleFiringCounts(traces, 10);
// Assert
result.VexOverrides.TotalOverrides.Should().Be(3);
result.VexOverrides.ByVendor["vendor_a"].Should().Be(2);
result.VexOverrides.ByVendor["vendor_b"].Should().Be(1);
result.VexOverrides.ByStatus["not_affected"].Should().Be(2);
result.VexOverrides.ByStatus["fixed"].Should().Be(1);
}
[Fact]
public void ComputeHeatmap_RuleSeverityMatrix_BuildsCorrectly()
{
// Arrange
var traces = new[]
{
CreateTrace("rule_a", 1, "block", expressionResult: true, severity: "critical"),
CreateTrace("rule_a", 1, "block", expressionResult: true, severity: "critical"),
CreateTrace("rule_a", 1, "block", expressionResult: true, severity: "high"),
CreateTrace("rule_b", 2, "warn", expressionResult: true, severity: "medium"),
};
var findings = CreateFindings(4);
// Act
var result = _service.ComputeHeatmap(traces, findings, SimulationAnalyticsOptions.Default);
// Assert
result.RuleSeverityMatrix.Should().NotBeEmpty();
var criticalCell = result.RuleSeverityMatrix.FirstOrDefault(c => c.X == "rule_a" && c.Y == "critical");
criticalCell.Should().NotBeNull();
criticalCell!.Value.Should().Be(2);
}
[Fact]
public void ComputeHeatmap_FindingRuleCoverage_CalculatesCorrectly()
{
// Arrange
var traces = new[]
{
CreateTrace("rule_a", 1, "block", expressionResult: true, componentPurl: "pkg:npm/lodash@4.0.0"),
CreateTrace("rule_b", 2, "allow", expressionResult: true, componentPurl: "pkg:npm/lodash@4.0.0"),
CreateTrace("rule_a", 1, "block", expressionResult: false, componentPurl: "pkg:npm/express@5.0.0"),
};
var findings = new[]
{
new SimulationFinding("f1", "pkg:npm/lodash@4.0.0", "GHSA-123", new Dictionary<string, object?>()),
new SimulationFinding("f2", "pkg:npm/express@5.0.0", "GHSA-456", new Dictionary<string, object?>()),
new SimulationFinding("f3", "pkg:npm/axios@1.0.0", "GHSA-789", new Dictionary<string, object?>()),
};
// Act
var result = _service.ComputeHeatmap(traces, findings, SimulationAnalyticsOptions.Default);
// Assert
result.FindingRuleCoverage.TotalFindings.Should().Be(3);
result.FindingRuleCoverage.FindingsMatched.Should().Be(1);
result.FindingRuleCoverage.CoveragePercentage.Should().BeApproximately(33.33, 0.1);
}
[Fact]
public void ComputeSampledTraces_DeterministicOrdering_OrdersByFindingId()
{
// Arrange
var traces = new[]
{
CreateTrace("rule_a", 1, "block", expressionResult: true, componentPurl: "pkg:npm/z-package@1.0.0"),
CreateTrace("rule_a", 1, "allow", expressionResult: true, componentPurl: "pkg:npm/a-package@1.0.0"),
CreateTrace("rule_b", 2, "warn", expressionResult: true, componentPurl: "pkg:npm/m-package@1.0.0"),
};
var findings = new[]
{
new SimulationFinding("finding-z", "pkg:npm/z-package@1.0.0", null, new Dictionary<string, object?>()),
new SimulationFinding("finding-a", "pkg:npm/a-package@1.0.0", null, new Dictionary<string, object?>()),
new SimulationFinding("finding-m", "pkg:npm/m-package@1.0.0", null, new Dictionary<string, object?>()),
};
var options = new SimulationAnalyticsOptions { TraceSampleRate = 1.0, MaxSampledTraces = 100 };
// Act
var result = _service.ComputeSampledTraces(traces, findings, options);
// Assert
result.Ordering.PrimaryKey.Should().Be("finding_id");
result.Ordering.Direction.Should().Be("ascending");
}
[Fact]
public void ComputeSampledTraces_DeterminismHash_ConsistentForSameInput()
{
// Arrange
var traces = new[]
{
CreateTrace("rule_a", 1, "block", expressionResult: true, componentPurl: "pkg:npm/lodash@4.0.0"),
};
var findings = new[]
{
new SimulationFinding("f1", "pkg:npm/lodash@4.0.0", "GHSA-123", new Dictionary<string, object?>()),
};
var options = new SimulationAnalyticsOptions { TraceSampleRate = 1.0 };
// Act
var result1 = _service.ComputeSampledTraces(traces, findings, options);
var result2 = _service.ComputeSampledTraces(traces, findings, options);
// Assert
result1.DeterminismHash.Should().Be(result2.DeterminismHash);
}
[Fact]
public void ComputeSampledTraces_HighSeverity_AlwaysSampled()
{
// Arrange
var traces = new[]
{
CreateTrace("rule_a", 1, "block", expressionResult: true, componentPurl: "pkg:npm/critical@1.0.0", severity: "critical"),
};
var findings = new[]
{
new SimulationFinding("f1", "pkg:npm/critical@1.0.0", null, new Dictionary<string, object?>()),
};
var options = new SimulationAnalyticsOptions { TraceSampleRate = 0.0 }; // Zero base rate
// Act
var result = _service.ComputeSampledTraces(traces, findings, options);
// Assert
result.SampledCount.Should().BeGreaterThan(0);
result.Traces.Should().Contain(t => t.SampleReason == "high_severity");
}
[Fact]
public void ComputeDeltaSummary_OutcomeChanges_CalculatesCorrectly()
{
// Arrange
var baseResults = new[]
{
new SimulationFindingResult("f1", "pkg:a", null, "block", "critical", new[] { "rule_a" }),
new SimulationFindingResult("f2", "pkg:b", null, "warn", "medium", new[] { "rule_b" }),
new SimulationFindingResult("f3", "pkg:c", null, "allow", "low", new[] { "rule_c" }),
};
var candidateResults = new[]
{
new SimulationFindingResult("f1", "pkg:a", null, "warn", "high", new[] { "rule_a" }), // Improved
new SimulationFindingResult("f2", "pkg:b", null, "block", "critical", new[] { "rule_b" }), // Regressed
new SimulationFindingResult("f3", "pkg:c", null, "allow", "low", new[] { "rule_c" }), // Unchanged
};
// Act
var result = _service.ComputeDeltaSummary("v1", "v2", baseResults, candidateResults);
// Assert
result.OutcomeChanges.Unchanged.Should().Be(1);
result.OutcomeChanges.Improved.Should().Be(1);
result.OutcomeChanges.Regressed.Should().Be(1);
result.OutcomeChanges.Transitions.Should().HaveCount(2);
}
[Fact]
public void ComputeDeltaSummary_SeverityChanges_TracksEscalationAndDeescalation()
{
// Arrange
var baseResults = new[]
{
new SimulationFindingResult("f1", "pkg:a", null, "block", "medium", Array.Empty<string>()),
new SimulationFindingResult("f2", "pkg:b", null, "block", "high", Array.Empty<string>()),
new SimulationFindingResult("f3", "pkg:c", null, "warn", "low", Array.Empty<string>()),
};
var candidateResults = new[]
{
new SimulationFindingResult("f1", "pkg:a", null, "block", "critical", Array.Empty<string>()), // Escalated
new SimulationFindingResult("f2", "pkg:b", null, "block", "medium", Array.Empty<string>()), // Deescalated
new SimulationFindingResult("f3", "pkg:c", null, "warn", "low", Array.Empty<string>()), // Unchanged
};
// Act
var result = _service.ComputeDeltaSummary("v1", "v2", baseResults, candidateResults);
// Assert
result.SeverityChanges.Unchanged.Should().Be(1);
result.SeverityChanges.Escalated.Should().Be(1);
result.SeverityChanges.Deescalated.Should().Be(1);
}
[Fact]
public void ComputeDeltaSummary_RuleChanges_DetectsAddedAndRemovedRules()
{
// Arrange
var baseResults = new[]
{
new SimulationFindingResult("f1", "pkg:a", null, "block", "high", new[] { "rule_old", "rule_common" }),
};
var candidateResults = new[]
{
new SimulationFindingResult("f1", "pkg:a", null, "block", "high", new[] { "rule_new", "rule_common" }),
};
// Act
var result = _service.ComputeDeltaSummary("v1", "v2", baseResults, candidateResults);
// Assert
result.RuleChanges.RulesAdded.Should().Contain("rule_new");
result.RuleChanges.RulesRemoved.Should().Contain("rule_old");
}
[Fact]
public void ComputeDeltaSummary_HighImpactFindings_IdentifiedCorrectly()
{
// Arrange
var baseResults = new[]
{
new SimulationFindingResult("f1", "pkg:critical", "CVE-2024-001", "allow", "low", Array.Empty<string>()),
};
var candidateResults = new[]
{
new SimulationFindingResult("f1", "pkg:critical", "CVE-2024-001", "block", "critical", Array.Empty<string>()),
};
// Act
var result = _service.ComputeDeltaSummary("v1", "v2", baseResults, candidateResults);
// Assert
result.HighImpactFindings.Should().NotBeEmpty();
result.HighImpactFindings[0].FindingId.Should().Be("f1");
result.HighImpactFindings[0].ImpactScore.Should().BeGreaterThan(0.5);
}
[Fact]
public void ComputeDeltaSummary_DeterminismHash_ConsistentForSameInput()
{
// Arrange
var baseResults = new[]
{
new SimulationFindingResult("f1", "pkg:a", null, "block", "high", Array.Empty<string>()),
};
var candidateResults = new[]
{
new SimulationFindingResult("f1", "pkg:a", null, "warn", "medium", Array.Empty<string>()),
};
// Act
var result1 = _service.ComputeDeltaSummary("v1", "v2", baseResults, candidateResults);
var result2 = _service.ComputeDeltaSummary("v1", "v2", baseResults, candidateResults);
// Assert
result1.DeterminismHash.Should().Be(result2.DeterminismHash);
}
[Fact]
public void ComputeAnalytics_FullAnalysis_ReturnsAllComponents()
{
// Arrange
var traces = new[]
{
CreateTrace("rule_a", 1, "block", expressionResult: true, componentPurl: "pkg:npm/lodash@4.0.0", severity: "high"),
CreateTrace("rule_b", 2, "allow", expressionResult: true, componentPurl: "pkg:npm/express@5.0.0", severity: "low"),
};
var findings = new[]
{
new SimulationFinding("f1", "pkg:npm/lodash@4.0.0", "GHSA-123", new Dictionary<string, object?>()),
new SimulationFinding("f2", "pkg:npm/express@5.0.0", "GHSA-456", new Dictionary<string, object?>()),
};
// Act
var result = _service.ComputeAnalytics("policy-v1", traces, findings);
// Assert
result.RuleFiringCounts.Should().NotBeNull();
result.Heatmap.Should().NotBeNull();
result.SampledTraces.Should().NotBeNull();
result.DeltaSummary.Should().BeNull(); // No delta for single policy analysis
}
private static RuleHitTrace CreateTrace(
string ruleName,
int priority,
string outcome,
bool expressionResult,
string? severity = null,
bool isVexOverride = false,
string? vexVendor = null,
string? vexStatus = null,
string? componentPurl = null)
{
return new RuleHitTrace
{
TraceId = Guid.NewGuid().ToString(),
SpanId = Guid.NewGuid().ToString("N")[..16],
TenantId = "test-tenant",
PolicyId = "test-policy",
RunId = "test-run",
RuleName = ruleName,
RulePriority = priority,
Outcome = outcome,
AssignedSeverity = severity,
ComponentPurl = componentPurl,
ExpressionResult = expressionResult,
EvaluationTimestamp = DateTimeOffset.UtcNow,
RecordedAt = DateTimeOffset.UtcNow,
EvaluationMicroseconds = 100,
IsVexOverride = isVexOverride,
VexVendor = vexVendor,
VexStatus = vexStatus,
IsSampled = true,
Attributes = ImmutableDictionary<string, string>.Empty
};
}
private static SimulationFinding[] CreateFindings(int count)
{
return Enumerable.Range(1, count)
.Select(i => new SimulationFinding(
$"finding-{i}",
$"pkg:npm/package-{i}@1.0.0",
$"GHSA-{i:D3}",
new Dictionary<string, object?>()))
.ToArray();
}
}

View File

@@ -0,0 +1,301 @@
using System.Collections.Immutable;
using FluentAssertions;
using Microsoft.Extensions.Time.Testing;
using StellaOps.Policy.Engine.Telemetry;
using Xunit;
namespace StellaOps.Policy.Engine.Tests.Telemetry;
public sealed class TelemetryTests
{
#region RuleHitTrace Tests
[Fact]
public void RuleHitTrace_GetOrCreateTraceId_ReturnsValidId()
{
var traceId = RuleHitTrace.GetOrCreateTraceId();
traceId.Should().NotBeNullOrEmpty();
traceId.Should().HaveLength(32); // 16 bytes = 32 hex chars
}
[Fact]
public void RuleHitTrace_GetOrCreateSpanId_ReturnsValidId()
{
var spanId = RuleHitTrace.GetOrCreateSpanId();
spanId.Should().NotBeNullOrEmpty();
spanId.Should().HaveLength(16); // 8 bytes = 16 hex chars
}
[Fact]
public void RuleHitTrace_GetOrCreateTraceId_GeneratesUniqueIds()
{
var ids = Enumerable.Range(0, 100)
.Select(_ => RuleHitTrace.GetOrCreateTraceId())
.ToList();
ids.Distinct().Should().HaveCount(100);
}
#endregion
#region RuleHitTraceFactory Tests
[Fact]
public void Create_ProducesValidTrace()
{
var timestamp = DateTimeOffset.UtcNow;
var timeProvider = new FakeTimeProvider(timestamp);
var trace = RuleHitTraceFactory.Create(
tenantId: "TENANT-1",
policyId: "policy-1",
policyVersion: 2,
runId: "run-123",
ruleName: "block-critical",
rulePriority: 10,
outcome: "deny",
evaluationTimestamp: timestamp,
timeProvider: timeProvider,
ruleCategory: "severity",
assignedSeverity: "Critical",
componentPurl: "pkg:npm/lodash@4.17.21",
advisoryId: "GHSA-test-001",
vulnerabilityId: "CVE-2021-12345");
trace.TenantId.Should().Be("tenant-1"); // Normalized to lowercase
trace.PolicyId.Should().Be("policy-1");
trace.PolicyVersion.Should().Be(2);
trace.RunId.Should().Be("run-123");
trace.RuleName.Should().Be("block-critical");
trace.RulePriority.Should().Be(10);
trace.Outcome.Should().Be("deny");
trace.RuleCategory.Should().Be("severity");
trace.AssignedSeverity.Should().Be("Critical");
trace.ComponentPurl.Should().Be("pkg:npm/lodash@4.17.21");
trace.EvaluationTimestamp.Should().Be(timestamp);
trace.RecordedAt.Should().Be(timestamp);
trace.TraceId.Should().NotBeNullOrEmpty();
trace.SpanId.Should().NotBeNullOrEmpty();
}
[Fact]
public void Create_TracksVexOverride()
{
var timestamp = DateTimeOffset.UtcNow;
var trace = RuleHitTraceFactory.Create(
tenantId: "tenant-1",
policyId: "policy-1",
policyVersion: 1,
runId: "run-123",
ruleName: "vex-override",
rulePriority: 1,
outcome: "suppress",
evaluationTimestamp: timestamp,
vexStatus: "not_affected",
vexJustification: "vulnerable_code_not_in_execute_path",
vexVendor: "vendor-1",
isVexOverride: true);
trace.VexStatus.Should().Be("not_affected");
trace.VexJustification.Should().Be("vulnerable_code_not_in_execute_path");
trace.VexVendor.Should().Be("vendor-1");
trace.IsVexOverride.Should().BeTrue();
}
[Fact]
public void Create_TracksReachability()
{
var timestamp = DateTimeOffset.UtcNow;
var trace = RuleHitTraceFactory.Create(
tenantId: "tenant-1",
policyId: "policy-1",
policyVersion: 1,
runId: "run-123",
ruleName: "reachability-rule",
rulePriority: 5,
outcome: "allow",
evaluationTimestamp: timestamp,
reachabilityState: "reachable",
reachabilityConfidence: 0.95);
trace.ReachabilityState.Should().Be("reachable");
trace.ReachabilityConfidence.Should().Be(0.95);
}
[Fact]
public void Create_IncludesCustomAttributes()
{
var timestamp = DateTimeOffset.UtcNow;
var attributes = ImmutableDictionary<string, string>.Empty
.Add("custom_key", "custom_value")
.Add("another_key", "another_value");
var trace = RuleHitTraceFactory.Create(
tenantId: "tenant-1",
policyId: "policy-1",
policyVersion: 1,
runId: "run-123",
ruleName: "test-rule",
rulePriority: 1,
outcome: "allow",
evaluationTimestamp: timestamp,
attributes: attributes);
trace.Attributes.Should().ContainKey("custom_key");
trace.Attributes["custom_key"].Should().Be("custom_value");
}
[Fact]
public void ToJson_ProducesValidJson()
{
var trace = RuleHitTraceFactory.Create(
tenantId: "tenant-1",
policyId: "policy-1",
policyVersion: 1,
runId: "run-123",
ruleName: "test-rule",
rulePriority: 1,
outcome: "allow",
evaluationTimestamp: DateTimeOffset.UtcNow);
var json = RuleHitTraceFactory.ToJson(trace);
json.Should().Contain("\"tenant_id\":\"tenant-1\"");
json.Should().Contain("\"policy_id\":\"policy-1\"");
json.Should().Contain("\"rule_name\":\"test-rule\"");
json.Should().NotContain("\n"); // Single line
}
[Fact]
public void ToNdjson_ProducesMultipleLines()
{
var timestamp = DateTimeOffset.UtcNow;
var traces = new[]
{
RuleHitTraceFactory.Create("tenant-1", "policy-1", 1, "run-1", "rule-1", 1, "allow", timestamp),
RuleHitTraceFactory.Create("tenant-1", "policy-1", 1, "run-1", "rule-2", 2, "deny", timestamp),
RuleHitTraceFactory.Create("tenant-1", "policy-1", 1, "run-1", "rule-3", 3, "suppress", timestamp)
};
var ndjson = RuleHitTraceFactory.ToNdjson(traces);
var lines = ndjson.Split('\n', StringSplitOptions.RemoveEmptyEntries);
lines.Should().HaveCount(3);
lines[0].Should().Contain("rule-1");
lines[1].Should().Contain("rule-2");
lines[2].Should().Contain("rule-3");
}
#endregion
#region RuleHitStatistics Tests
[Fact]
public void CreateStatistics_AggregatesCorrectly()
{
var timestamp = DateTimeOffset.UtcNow;
var traces = new[]
{
RuleHitTraceFactory.Create("tenant-1", "policy-1", 1, "run-1", "rule-1", 1, "allow", timestamp,
ruleCategory: "severity"),
RuleHitTraceFactory.Create("tenant-1", "policy-1", 1, "run-1", "rule-2", 2, "deny", timestamp,
ruleCategory: "severity"),
RuleHitTraceFactory.Create("tenant-1", "policy-1", 1, "run-1", "rule-3", 3, "suppress", timestamp,
ruleCategory: "vex", isVexOverride: true, vexVendor: "vendor-1", vexStatus: "not_affected"),
RuleHitTraceFactory.Create("tenant-1", "policy-1", 1, "run-1", "rule-4", 4, "suppress", timestamp,
ruleCategory: "vex", isVexOverride: true, vexVendor: "vendor-2", vexStatus: "fixed")
};
var stats = RuleHitTraceFactory.CreateStatistics(
runId: "run-1",
policyId: "policy-1",
traces: traces,
totalRulesEvaluated: 10,
totalEvaluationMs: 50);
stats.RunId.Should().Be("run-1");
stats.PolicyId.Should().Be("policy-1");
stats.TotalRulesEvaluated.Should().Be(10);
stats.TotalRulesFired.Should().Be(4);
stats.TotalVexOverrides.Should().Be(2);
stats.RulesFiredByCategory.Should().ContainKey("severity");
stats.RulesFiredByCategory["severity"].Should().Be(2);
stats.RulesFiredByCategory["vex"].Should().Be(2);
stats.RulesFiredByOutcome.Should().ContainKey("allow");
stats.RulesFiredByOutcome["allow"].Should().Be(1);
stats.RulesFiredByOutcome["deny"].Should().Be(1);
stats.RulesFiredByOutcome["suppress"].Should().Be(2);
stats.VexOverridesByVendor.Should().HaveCount(2);
stats.VexOverridesByStatus.Should().ContainKey("not_affected");
stats.VexOverridesByStatus.Should().ContainKey("fixed");
}
[Fact]
public void CreateStatistics_ComputesAverageEvaluationTime()
{
var traces = Array.Empty<RuleHitTrace>();
var stats = RuleHitTraceFactory.CreateStatistics(
runId: "run-1",
policyId: "policy-1",
traces: traces,
totalRulesEvaluated: 100,
totalEvaluationMs: 50);
stats.TotalEvaluationMs.Should().Be(50);
stats.AverageRuleEvaluationMicroseconds.Should().Be(500); // 50ms * 1000 / 100 rules
}
[Fact]
public void CreateStatistics_HandlesZeroRules()
{
var traces = Array.Empty<RuleHitTrace>();
var stats = RuleHitTraceFactory.CreateStatistics(
runId: "run-1",
policyId: "policy-1",
traces: traces,
totalRulesEvaluated: 0,
totalEvaluationMs: 0);
stats.TotalRulesEvaluated.Should().Be(0);
stats.AverageRuleEvaluationMicroseconds.Should().Be(0);
}
[Fact]
public void CreateStatistics_GeneratesTopRules()
{
var timestamp = DateTimeOffset.UtcNow;
var traces = Enumerable.Range(0, 20)
.SelectMany(i => Enumerable.Range(0, i + 1).Select(_ =>
RuleHitTraceFactory.Create("tenant-1", "policy-1", 1, "run-1", $"rule-{i}", i, "allow", timestamp)))
.ToArray();
var stats = RuleHitTraceFactory.CreateStatistics("run-1", "policy-1", traces, 100, 50);
stats.TopRulesByHitCount.Should().HaveCount(10);
stats.TopRulesByHitCount[0].RuleName.Should().Be("rule-19"); // Highest count
stats.TopRulesByHitCount[0].HitCount.Should().Be(20);
}
#endregion
#region RuleHitCount Tests
[Fact]
public void RuleHitCount_RecordWorks()
{
var hitCount = new RuleHitCount("severity-rule", 42, "deny");
hitCount.RuleName.Should().Be("severity-rule");
hitCount.HitCount.Should().Be(42);
hitCount.Outcome.Should().Be("deny");
}
#endregion
}