up
Some checks failed
api-governance / spectral-lint (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
Some checks failed
api-governance / spectral-lint (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
using StellaOps.Policy.Engine.Domain;
|
||||
using StellaOps.Policy.Engine.Services;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Tests;
|
||||
|
||||
public sealed class EvidenceSummaryServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void Summarize_BuildsDeterministicSummary()
|
||||
{
|
||||
var timeProvider = new FixedTimeProvider(new DateTimeOffset(2025, 11, 26, 0, 0, 0, TimeSpan.Zero));
|
||||
var service = new EvidenceSummaryService(timeProvider);
|
||||
|
||||
var request = new EvidenceSummaryRequest(
|
||||
EvidenceHash: "stub-evidence-hash",
|
||||
FilePath: "/etc/passwd",
|
||||
Digest: "sha256:123",
|
||||
IngestedAt: null,
|
||||
ConnectorId: "connector-1");
|
||||
|
||||
var response = service.Summarize(request);
|
||||
|
||||
Assert.Equal("stub-evidence-hash", response.EvidenceHash);
|
||||
Assert.Equal("info", response.Summary.Severity); // first byte bucketed to info
|
||||
Assert.Equal("/etc/passwd", response.Summary.Locator.FilePath);
|
||||
Assert.Equal("sha256:123", response.Summary.Locator.Digest);
|
||||
Assert.Equal("connector-1", response.Summary.Provenance.ConnectorId);
|
||||
Assert.Equal(new DateTimeOffset(2025, 12, 13, 05, 00, 11, TimeSpan.Zero), response.Summary.Provenance.IngestedAt);
|
||||
Assert.Contains("stub-eviden", response.Summary.Headline);
|
||||
Assert.Equal(
|
||||
new[] { "severity:info", "path:/etc/passwd", "connector:connector-1" },
|
||||
response.Summary.Signals);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Summarize_RequiresEvidenceHash()
|
||||
{
|
||||
var timeProvider = new FixedTimeProvider(DateTimeOffset.UnixEpoch);
|
||||
var service = new EvidenceSummaryService(timeProvider);
|
||||
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
service.Summarize(new EvidenceSummaryRequest(string.Empty, null, null, null, null)));
|
||||
}
|
||||
|
||||
private sealed class FixedTimeProvider : TimeProvider
|
||||
{
|
||||
private readonly DateTimeOffset _now;
|
||||
|
||||
public FixedTimeProvider(DateTimeOffset now) => _now = now;
|
||||
|
||||
public override DateTimeOffset GetUtcNow() => _now;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Policy;
|
||||
using StellaOps.Policy.Engine.Domain;
|
||||
using StellaOps.Policy.Engine.Options;
|
||||
using StellaOps.Policy.Engine.Services;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Tests;
|
||||
|
||||
public sealed class PolicyBundleServiceTests
|
||||
{
|
||||
private const string BaselineDsl = """
|
||||
policy "Baseline Production Policy" syntax "stella-dsl@1" {
|
||||
rule r1 { when true then status := "ok" because "baseline" }
|
||||
}
|
||||
""";
|
||||
|
||||
[Fact]
|
||||
public async Task CompileAndStoreAsync_SucceedsAndStoresBundle()
|
||||
{
|
||||
var services = CreateServices();
|
||||
var request = new PolicyBundleRequest(new PolicyDslPayload("stella-dsl@1", BaselineDsl), signingKeyId: "test-key");
|
||||
|
||||
var response = await services.BundleService.CompileAndStoreAsync("pack-1", 1, request, CancellationToken.None);
|
||||
|
||||
Assert.True(response.Success);
|
||||
Assert.NotNull(response.Digest);
|
||||
Assert.StartsWith("sig:sha256:", response.Signature);
|
||||
Assert.True(response.SizeBytes > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompileAndStoreAsync_FailsWithBadSyntax()
|
||||
{
|
||||
var services = CreateServices();
|
||||
var request = new PolicyBundleRequest(new PolicyDslPayload("unknown", "policy bad"), signingKeyId: null);
|
||||
|
||||
var response = await services.BundleService.CompileAndStoreAsync("pack-1", 1, request, CancellationToken.None);
|
||||
|
||||
Assert.False(response.Success);
|
||||
Assert.Null(response.Digest);
|
||||
Assert.NotEmpty(response.Diagnostics);
|
||||
}
|
||||
|
||||
private static ServiceHarness CreateServices()
|
||||
{
|
||||
var compiler = new PolicyCompiler();
|
||||
var complexity = new PolicyComplexityAnalyzer();
|
||||
var options = Options.Create(new PolicyEngineOptions());
|
||||
var compilationService = new PolicyCompilationService(compiler, complexity, new StaticOptionsMonitor(options.Value), TimeProvider.System);
|
||||
var repo = new InMemoryPolicyPackRepository();
|
||||
return new ServiceHarness(
|
||||
new PolicyBundleService(compilationService, repo, TimeProvider.System));
|
||||
}
|
||||
|
||||
private sealed record ServiceHarness(PolicyBundleService BundleService);
|
||||
|
||||
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() { }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Policy.Engine.Domain;
|
||||
using StellaOps.Policy.Engine.Services;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Tests;
|
||||
|
||||
public sealed class PolicyRuntimeEvaluatorTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_ReturnsDeterministicDecisionAndCaches()
|
||||
{
|
||||
var repo = new InMemoryPolicyPackRepository();
|
||||
await repo.StoreBundleAsync(
|
||||
"pack-1",
|
||||
1,
|
||||
new PolicyBundleRecord(
|
||||
Digest: "sha256:abc",
|
||||
Signature: "sig:sha256:abc",
|
||||
Size: 4,
|
||||
CreatedAt: DateTimeOffset.UnixEpoch,
|
||||
Payload: new byte[] { 1, 2, 3, 4 }.ToImmutableArray()),
|
||||
CancellationToken.None);
|
||||
|
||||
var evaluator = new PolicyRuntimeEvaluator(repo);
|
||||
var request = new PolicyEvaluationRequest("pack-1", 1, "subject-a");
|
||||
|
||||
var first = await evaluator.EvaluateAsync(request, CancellationToken.None);
|
||||
var second = await evaluator.EvaluateAsync(request, CancellationToken.None);
|
||||
|
||||
Assert.Equal(first.Decision, second.Decision);
|
||||
Assert.False(first.Cached);
|
||||
Assert.True(second.Cached);
|
||||
Assert.Equal("pack-1", first.PackId);
|
||||
Assert.Equal(1, first.Version);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_ThrowsWhenBundleMissing()
|
||||
{
|
||||
var evaluator = new PolicyRuntimeEvaluator(new InMemoryPolicyPackRepository());
|
||||
var request = new PolicyEvaluationRequest("pack-x", 1, "subject-a");
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => evaluator.EvaluateAsync(request, CancellationToken.None));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Policy.RiskProfile.Canonicalization;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.RiskProfile.Tests;
|
||||
|
||||
public class RiskProfileCanonicalizerTests
|
||||
{
|
||||
[Fact]
|
||||
public void Canonicalize_SortsSignalsAndOverrides()
|
||||
{
|
||||
const string input = """
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"id": "profile",
|
||||
"signals": [
|
||||
{"name": "kev", "source": "cisa", "type": "boolean"},
|
||||
{"name": "reachability", "type": "boolean", "source": "signals"}
|
||||
],
|
||||
"weights": {"reachability": 0.6, "kev": 0.4},
|
||||
"overrides": {
|
||||
"severity": [
|
||||
{"when": {"kev": true}, "set": "critical"},
|
||||
{"when": {"reachability": false}, "set": "low"}
|
||||
],
|
||||
"decisions": [
|
||||
{"when": {"reachability": false}, "action": "review"},
|
||||
{"when": {"kev": true}, "action": "deny"}
|
||||
]
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var canonical = RiskProfileCanonicalizer.CanonicalizeToString(input);
|
||||
|
||||
const string expected = "{\"id\":\"profile\",\"overrides\":{\"decisions\":[{\"action\":\"deny\",\"when\":{\"kev\":true}},{\"action\":\"review\",\"when\":{\"reachability\":false}}],\"severity\":[{\"set\":\"critical\",\"when\":{\"kev\":true}},{\"set\":\"low\",\"when\":{\"reachability\":false}}]},\"signals\":[{\"name\":\"kev\",\"source\":\"cisa\",\"type\":\"boolean\"},{\"name\":\"reachability\",\"source\":\"signals\",\"type\":\"boolean\"}],\"version\":\"1.0.0\",\"weights\":{\"kev\":0.4,\"reachability\":0.6}}";
|
||||
|
||||
Assert.Equal(expected, canonical);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeDigest_IgnoresOrderingNoise()
|
||||
{
|
||||
const string a = """
|
||||
{"id":"p","version":"1.0.0","signals":[{"name":"b","source":"x","type":"boolean"},{"name":"a","source":"y","type":"boolean"}],"weights":{"b":0.5,"a":0.5},"overrides":{"severity":[{"when":{"a":true},"set":"high"}],"decisions":[{"when":{"b":false},"action":"review"}]}}
|
||||
""";
|
||||
const string b = """
|
||||
{"version":"1.0.0","id":"p","weights":{"a":0.5,"b":0.5},"signals":[{"source":"y","name":"a","type":"boolean"},{"type":"boolean","name":"b","source":"x"}],"overrides":{"decisions":[{"action":"review","when":{"b":false}}],"severity":[{"set":"high","when":{"a":true}}]}}
|
||||
""";
|
||||
|
||||
var hashA = RiskProfileCanonicalizer.ComputeDigest(a);
|
||||
var hashB = RiskProfileCanonicalizer.ComputeDigest(b);
|
||||
|
||||
Assert.Equal(hashA, hashB);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Merge_ReplacesSignalsAndWeights()
|
||||
{
|
||||
const string baseProfile = """
|
||||
{"id":"p","version":"1.0.0","signals":[{"name":"reachability","source":"signals","type":"boolean"}],"weights":{"reachability":0.7},"overrides":{"decisions":[{"when":{"reachability":false},"action":"review"}]}}
|
||||
""";
|
||||
|
||||
const string overlay = """
|
||||
{"signals":[{"name":"kev","source":"cisa","type":"boolean"}],"weights":{"kev":0.5},"overrides":{"decisions":[{"when":{"kev":true},"action":"deny"}]}}
|
||||
""";
|
||||
|
||||
var merged = RiskProfileCanonicalizer.Merge(baseProfile, overlay);
|
||||
using var doc = JsonDocument.Parse(merged);
|
||||
var root = doc.RootElement;
|
||||
|
||||
Assert.Equal(2, root.GetProperty("signals").GetArrayLength());
|
||||
Assert.Equal(2, root.GetProperty("weights").EnumerateObject().Count());
|
||||
|
||||
var decisions = root.GetProperty("overrides").GetProperty("decisions").EnumerateArray().ToArray();
|
||||
Assert.Contains(decisions, d => d.GetProperty("action").GetString() == "deny");
|
||||
Assert.Contains(decisions, d => d.GetProperty("action").GetString() == "review");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user