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
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
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
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Policy.Engine.Caching;
|
||||
using StellaOps.Policy.Engine.Compilation;
|
||||
using StellaOps.Policy.Engine.Domain;
|
||||
using StellaOps.Policy.Engine.Evaluation;
|
||||
using StellaOps.Policy.Engine.Options;
|
||||
using StellaOps.Policy.Engine.Services;
|
||||
using StellaOps.PolicyDsl;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Tests;
|
||||
|
||||
public sealed class PolicyRuntimeEvaluationServiceTests
|
||||
{
|
||||
private const string TestPolicy = """
|
||||
policy "Test Policy" syntax "stella-dsl@1" {
|
||||
rule block_critical priority 10 {
|
||||
when severity.normalized == "Critical"
|
||||
then status := "blocked"
|
||||
because "Block critical findings"
|
||||
}
|
||||
|
||||
rule warn_high priority 20 {
|
||||
when severity.normalized == "High"
|
||||
then status := "warn"
|
||||
because "Warn on high severity findings"
|
||||
}
|
||||
|
||||
rule allow_default priority 100 {
|
||||
when true
|
||||
then status := "affected"
|
||||
because "Default affected status"
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_ReturnsDecisionFromCompiledPolicy()
|
||||
{
|
||||
var harness = CreateHarness();
|
||||
await harness.StoreTestPolicyAsync("pack-1", 1, TestPolicy);
|
||||
|
||||
var request = CreateRequest("pack-1", 1, severity: "Critical");
|
||||
|
||||
var response = await harness.Service.EvaluateAsync(request, CancellationToken.None);
|
||||
|
||||
Assert.Equal("pack-1", response.PackId);
|
||||
Assert.Equal(1, response.Version);
|
||||
Assert.NotNull(response.PolicyDigest);
|
||||
Assert.False(response.Cached);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_UsesCacheOnSecondCall()
|
||||
{
|
||||
var harness = CreateHarness();
|
||||
await harness.StoreTestPolicyAsync("pack-1", 1, TestPolicy);
|
||||
|
||||
var request = CreateRequest("pack-1", 1, severity: "High");
|
||||
|
||||
// First call - cache miss
|
||||
var response1 = await harness.Service.EvaluateAsync(request, CancellationToken.None);
|
||||
Assert.False(response1.Cached);
|
||||
|
||||
// Second call - cache hit
|
||||
var response2 = await harness.Service.EvaluateAsync(request, CancellationToken.None);
|
||||
Assert.True(response2.Cached);
|
||||
Assert.Equal(CacheSource.InMemory, response2.CacheSource);
|
||||
Assert.Equal(response1.Status, response2.Status);
|
||||
Assert.Equal(response1.CorrelationId, response2.CorrelationId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_BypassCacheWhenRequested()
|
||||
{
|
||||
var harness = CreateHarness();
|
||||
await harness.StoreTestPolicyAsync("pack-1", 1, TestPolicy);
|
||||
|
||||
var request = CreateRequest("pack-1", 1, severity: "Medium");
|
||||
|
||||
// First call
|
||||
var response1 = await harness.Service.EvaluateAsync(request, CancellationToken.None);
|
||||
Assert.False(response1.Cached);
|
||||
|
||||
// Second call with bypass
|
||||
var bypassRequest = request with { BypassCache = true };
|
||||
var response2 = await harness.Service.EvaluateAsync(bypassRequest, CancellationToken.None);
|
||||
Assert.False(response2.Cached);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_ThrowsOnMissingBundle()
|
||||
{
|
||||
var harness = CreateHarness();
|
||||
var request = CreateRequest("non-existent", 1, severity: "Low");
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => harness.Service.EvaluateAsync(request, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_GeneratesDeterministicCorrelationId()
|
||||
{
|
||||
var harness = CreateHarness();
|
||||
await harness.StoreTestPolicyAsync("pack-1", 1, TestPolicy);
|
||||
|
||||
var request = CreateRequest("pack-1", 1, severity: "High");
|
||||
|
||||
var response1 = await harness.Service.EvaluateAsync(request, CancellationToken.None);
|
||||
|
||||
// Create a new harness with fresh cache
|
||||
var harness2 = CreateHarness();
|
||||
await harness2.StoreTestPolicyAsync("pack-1", 1, TestPolicy);
|
||||
|
||||
var response2 = await harness2.Service.EvaluateAsync(request, CancellationToken.None);
|
||||
|
||||
// Same inputs should produce same correlation ID
|
||||
Assert.Equal(response1.CorrelationId, response2.CorrelationId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateBatchAsync_ReturnsMultipleResults()
|
||||
{
|
||||
var harness = CreateHarness();
|
||||
await harness.StoreTestPolicyAsync("pack-1", 1, TestPolicy);
|
||||
|
||||
var requests = new[]
|
||||
{
|
||||
CreateRequest("pack-1", 1, severity: "Critical", subjectPurl: "pkg:npm/lodash@4.17.0"),
|
||||
CreateRequest("pack-1", 1, severity: "High", subjectPurl: "pkg:npm/express@4.18.0"),
|
||||
CreateRequest("pack-1", 1, severity: "Medium", subjectPurl: "pkg:npm/axios@1.0.0"),
|
||||
};
|
||||
|
||||
var responses = await harness.Service.EvaluateBatchAsync(requests, CancellationToken.None);
|
||||
|
||||
Assert.Equal(3, responses.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateBatchAsync_UsesCacheForDuplicates()
|
||||
{
|
||||
var harness = CreateHarness();
|
||||
await harness.StoreTestPolicyAsync("pack-1", 1, TestPolicy);
|
||||
|
||||
// Pre-populate cache
|
||||
var request = CreateRequest("pack-1", 1, severity: "Critical");
|
||||
await harness.Service.EvaluateAsync(request, CancellationToken.None);
|
||||
|
||||
var requests = new[]
|
||||
{
|
||||
request, // Should be cached
|
||||
CreateRequest("pack-1", 1, severity: "High"), // New
|
||||
};
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_DifferentContextsGetDifferentCacheKeys()
|
||||
{
|
||||
var harness = CreateHarness();
|
||||
await harness.StoreTestPolicyAsync("pack-1", 1, TestPolicy);
|
||||
|
||||
var request1 = CreateRequest("pack-1", 1, severity: "High");
|
||||
var request2 = CreateRequest("pack-1", 1, severity: "Critical");
|
||||
|
||||
var response1 = await harness.Service.EvaluateAsync(request1, CancellationToken.None);
|
||||
var response2 = await harness.Service.EvaluateAsync(request2, CancellationToken.None);
|
||||
|
||||
// Both should be cache misses (different severity = different context)
|
||||
Assert.False(response1.Cached);
|
||||
Assert.False(response2.Cached);
|
||||
// Different inputs = different correlation IDs
|
||||
Assert.NotEqual(response1.CorrelationId, response2.CorrelationId);
|
||||
}
|
||||
|
||||
private static RuntimeEvaluationRequest CreateRequest(
|
||||
string packId,
|
||||
int version,
|
||||
string severity,
|
||||
string tenantId = "tenant-1",
|
||||
string subjectPurl = "pkg:npm/lodash@4.17.21",
|
||||
string advisoryId = "CVE-2024-0001")
|
||||
{
|
||||
return new RuntimeEvaluationRequest(
|
||||
packId,
|
||||
version,
|
||||
tenantId,
|
||||
subjectPurl,
|
||||
advisoryId,
|
||||
Severity: new PolicyEvaluationSeverity(severity, null),
|
||||
Advisory: new PolicyEvaluationAdvisory("NVD", ImmutableDictionary<string, string>.Empty),
|
||||
Vex: PolicyEvaluationVexEvidence.Empty,
|
||||
Sbom: PolicyEvaluationSbom.Empty,
|
||||
Exceptions: PolicyEvaluationExceptions.Empty,
|
||||
Reachability: PolicyEvaluationReachability.Unknown,
|
||||
EvaluationTimestamp: new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero),
|
||||
BypassCache: false);
|
||||
}
|
||||
|
||||
private static TestHarness CreateHarness()
|
||||
{
|
||||
var repository = new InMemoryPolicyPackRepository();
|
||||
var cacheLogger = NullLogger<InMemoryPolicyEvaluationCache>.Instance;
|
||||
var serviceLogger = NullLogger<PolicyRuntimeEvaluationService>.Instance;
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new PolicyEngineOptions());
|
||||
var cache = new InMemoryPolicyEvaluationCache(cacheLogger, TimeProvider.System, options);
|
||||
var evaluator = new PolicyEvaluator();
|
||||
|
||||
var compilationService = CreateCompilationService();
|
||||
|
||||
var service = new PolicyRuntimeEvaluationService(
|
||||
repository,
|
||||
cache,
|
||||
evaluator,
|
||||
TimeProvider.System,
|
||||
serviceLogger);
|
||||
|
||||
return new TestHarness(service, repository, compilationService);
|
||||
}
|
||||
|
||||
private static PolicyCompilationService CreateCompilationService()
|
||||
{
|
||||
var compiler = new PolicyCompiler();
|
||||
var analyzer = new PolicyComplexityAnalyzer();
|
||||
var options = new PolicyEngineOptions();
|
||||
var optionsMonitor = new StaticOptionsMonitor(options);
|
||||
return new PolicyCompilationService(compiler, analyzer, optionsMonitor, TimeProvider.System);
|
||||
}
|
||||
|
||||
private sealed record TestHarness(
|
||||
PolicyRuntimeEvaluationService Service,
|
||||
InMemoryPolicyPackRepository Repository,
|
||||
PolicyCompilationService CompilationService)
|
||||
{
|
||||
public async Task StoreTestPolicyAsync(string packId, int version, string dsl)
|
||||
{
|
||||
var bundleService = new PolicyBundleService(CompilationService, Repository, TimeProvider.System);
|
||||
var request = new PolicyBundleRequest(new PolicyDslPayload("stella-dsl@1", dsl), SigningKeyId: null);
|
||||
await bundleService.CompileAndStoreAsync(packId, version, request, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class StaticOptionsMonitor : IOptionsMonitor<PolicyEngineOptions>
|
||||
{
|
||||
private readonly PolicyEngineOptions _value;
|
||||
|
||||
public StaticOptionsMonitor(PolicyEngineOptions value) => _value = value;
|
||||
|
||||
public PolicyEngineOptions CurrentValue => _value;
|
||||
|
||||
public PolicyEngineOptions Get(string? name) => _value;
|
||||
|
||||
public IDisposable OnChange(Action<PolicyEngineOptions, string> listener) => NullDisposable.Instance;
|
||||
|
||||
private sealed class NullDisposable : IDisposable
|
||||
{
|
||||
public static readonly NullDisposable Instance = new();
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user