up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
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
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-13 00:20:26 +02:00
parent e1f1bef4c1
commit 564df71bfb
2376 changed files with 334389 additions and 328032 deletions

View File

@@ -1,104 +1,104 @@
using System.Collections.Immutable;
using System.Linq;
using StellaOps.Policy;
using StellaOps.PolicyDsl;
using Xunit;
using Xunit.Sdk;
namespace StellaOps.Policy.Engine.Tests;
public sealed class PolicyCompilerTests
{
[Fact]
public void Compile_BaselinePolicy_Succeeds()
{
const string source = """
policy "Baseline Production Policy" syntax "stella-dsl@1" {
metadata {
description = "Block critical, escalate high, enforce VEX justifications."
tags = ["baseline","production"]
}
profile severity {
map vendor_weight {
source "GHSA" => +0.5
source "OSV" => +0.0
}
env exposure_adjustments {
if env.exposure == "internet" then +0.5
}
}
rule block_critical priority 5 {
when severity.normalized >= "Critical"
then status := "blocked"
because "Critical severity must be remediated before deploy."
}
rule escalate_high_internet {
when severity.normalized == "High"
and env.exposure == "internet"
then escalate to severity_band("Critical")
because "High severity on internet-exposed asset escalates to critical."
}
rule require_vex_justification {
when vex.any(status in ["not_affected","fixed"])
and vex.justification in ["component_not_present","vulnerable_code_not_present"]
then status := vex.status
annotate winning_statement := vex.latest().statementId
because "Respect strong vendor VEX claims."
}
rule alert_warn_eol_runtime priority 1 {
when severity.normalized <= "Medium"
and sbom.has_tag("runtime:eol")
then warn message "Runtime marked as EOL; upgrade recommended."
because "Deprecated runtime should be upgraded."
}
}
""";
var compiler = new PolicyCompiler();
var result = compiler.Compile(source);
if (!result.Success)
{
throw new Xunit.Sdk.XunitException($"Compilation failed: {Describe(result.Diagnostics)}");
}
Assert.False(string.IsNullOrWhiteSpace(result.Checksum));
Assert.NotEmpty(result.CanonicalRepresentation);
Assert.All(result.Diagnostics, issue => Assert.NotEqual(PolicyIssueSeverity.Error, issue.Severity));
var document = Assert.IsType<PolicyIrDocument>(result.Document);
Assert.Equal("Baseline Production Policy", document.Name);
Assert.Equal("stella-dsl@1", document.Syntax);
Assert.Equal(4, document.Rules.Length);
Assert.Single(document.Profiles);
var firstAction = Assert.IsType<PolicyIrAssignmentAction>(document.Rules[0].ThenActions[0]);
Assert.Equal("status", firstAction.Target[0]);
}
[Fact]
public void Compile_MissingBecause_ReportsDiagnostic()
{
const string source = """
policy "Incomplete" syntax "stella-dsl@1" {
rule missing_because {
when true
then status := "suppressed"
}
}
""";
var compiler = new PolicyCompiler();
var result = compiler.Compile(source);
Assert.False(result.Success);
PolicyIssue diagnostic = result.Diagnostics.First(issue => issue.Code == "POLICY-DSL-PARSE-006");
Assert.Equal(PolicyIssueSeverity.Error, diagnostic.Severity);
}
private static string Describe(ImmutableArray<PolicyIssue> issues) =>
string.Join(" | ", issues.Select(issue => $"{issue.Severity}:{issue.Code}:{issue.Message}"));
}
using System.Collections.Immutable;
using System.Linq;
using StellaOps.Policy;
using StellaOps.PolicyDsl;
using Xunit;
using Xunit.Sdk;
namespace StellaOps.Policy.Engine.Tests;
public sealed class PolicyCompilerTests
{
[Fact]
public void Compile_BaselinePolicy_Succeeds()
{
const string source = """
policy "Baseline Production Policy" syntax "stella-dsl@1" {
metadata {
description = "Block critical, escalate high, enforce VEX justifications."
tags = ["baseline","production"]
}
profile severity {
map vendor_weight {
source "GHSA" => +0.5
source "OSV" => +0.0
}
env exposure_adjustments {
if env.exposure == "internet" then +0.5
}
}
rule block_critical priority 5 {
when severity.normalized >= "Critical"
then status := "blocked"
because "Critical severity must be remediated before deploy."
}
rule escalate_high_internet {
when severity.normalized == "High"
and env.exposure == "internet"
then escalate to severity_band("Critical")
because "High severity on internet-exposed asset escalates to critical."
}
rule require_vex_justification {
when vex.any(status in ["not_affected","fixed"])
and vex.justification in ["component_not_present","vulnerable_code_not_present"]
then status := vex.status
annotate winning_statement := vex.latest().statementId
because "Respect strong vendor VEX claims."
}
rule alert_warn_eol_runtime priority 1 {
when severity.normalized <= "Medium"
and sbom.has_tag("runtime:eol")
then warn message "Runtime marked as EOL; upgrade recommended."
because "Deprecated runtime should be upgraded."
}
}
""";
var compiler = new PolicyCompiler();
var result = compiler.Compile(source);
if (!result.Success)
{
throw new Xunit.Sdk.XunitException($"Compilation failed: {Describe(result.Diagnostics)}");
}
Assert.False(string.IsNullOrWhiteSpace(result.Checksum));
Assert.NotEmpty(result.CanonicalRepresentation);
Assert.All(result.Diagnostics, issue => Assert.NotEqual(PolicyIssueSeverity.Error, issue.Severity));
var document = Assert.IsType<PolicyIrDocument>(result.Document);
Assert.Equal("Baseline Production Policy", document.Name);
Assert.Equal("stella-dsl@1", document.Syntax);
Assert.Equal(4, document.Rules.Length);
Assert.Single(document.Profiles);
var firstAction = Assert.IsType<PolicyIrAssignmentAction>(document.Rules[0].ThenActions[0]);
Assert.Equal("status", firstAction.Target[0]);
}
[Fact]
public void Compile_MissingBecause_ReportsDiagnostic()
{
const string source = """
policy "Incomplete" syntax "stella-dsl@1" {
rule missing_because {
when true
then status := "suppressed"
}
}
""";
var compiler = new PolicyCompiler();
var result = compiler.Compile(source);
Assert.False(result.Success);
PolicyIssue diagnostic = result.Diagnostics.First(issue => issue.Code == "POLICY-DSL-PARSE-006");
Assert.Equal(PolicyIssueSeverity.Error, diagnostic.Severity);
}
private static string Describe(ImmutableArray<PolicyIssue> issues) =>
string.Join(" | ", issues.Select(issue => $"{issue.Severity}:{issue.Code}:{issue.Message}"));
}

View File

@@ -1,44 +1,44 @@
using StellaOps.Policy.Engine.Domain;
using StellaOps.Policy.Engine.Services;
using Xunit;
namespace StellaOps.Policy.Engine.Tests;
public class PolicyPackRepositoryTests
{
private readonly InMemoryPolicyPackRepository repository = new();
[Fact]
public async Task ActivateRevision_WithSingleApprover_ActivatesImmediately()
{
await repository.CreateAsync("pack-1", "Pack", CancellationToken.None);
await repository.UpsertRevisionAsync("pack-1", 1, requiresTwoPersonApproval: false, PolicyRevisionStatus.Approved, CancellationToken.None);
var result = await repository.RecordActivationAsync("pack-1", 1, "alice", DateTimeOffset.UtcNow, null, CancellationToken.None);
Assert.Equal(PolicyActivationResultStatus.Activated, result.Status);
Assert.NotNull(result.Revision);
Assert.Equal(PolicyRevisionStatus.Active, result.Revision!.Status);
Assert.Single(result.Revision.Approvals);
}
[Fact]
public async Task ActivateRevision_WithTwoPersonRequirement_ReturnsPendingUntilSecondApproval()
{
await repository.CreateAsync("pack-2", "Pack", CancellationToken.None);
await repository.UpsertRevisionAsync("pack-2", 1, requiresTwoPersonApproval: true, PolicyRevisionStatus.Approved, CancellationToken.None);
var first = await repository.RecordActivationAsync("pack-2", 1, "alice", DateTimeOffset.UtcNow, null, CancellationToken.None);
Assert.Equal(PolicyActivationResultStatus.PendingSecondApproval, first.Status);
Assert.Equal(PolicyRevisionStatus.Approved, first.Revision!.Status);
Assert.Single(first.Revision.Approvals);
var duplicate = await repository.RecordActivationAsync("pack-2", 1, "alice", DateTimeOffset.UtcNow, null, CancellationToken.None);
Assert.Equal(PolicyActivationResultStatus.DuplicateApproval, duplicate.Status);
var second = await repository.RecordActivationAsync("pack-2", 1, "bob", DateTimeOffset.UtcNow, null, CancellationToken.None);
Assert.Equal(PolicyActivationResultStatus.Activated, second.Status);
Assert.Equal(PolicyRevisionStatus.Active, second.Revision!.Status);
Assert.Equal(2, second.Revision.Approvals.Length);
}
}
using StellaOps.Policy.Engine.Domain;
using StellaOps.Policy.Engine.Services;
using Xunit;
namespace StellaOps.Policy.Engine.Tests;
public class PolicyPackRepositoryTests
{
private readonly InMemoryPolicyPackRepository repository = new();
[Fact]
public async Task ActivateRevision_WithSingleApprover_ActivatesImmediately()
{
await repository.CreateAsync("pack-1", "Pack", CancellationToken.None);
await repository.UpsertRevisionAsync("pack-1", 1, requiresTwoPersonApproval: false, PolicyRevisionStatus.Approved, CancellationToken.None);
var result = await repository.RecordActivationAsync("pack-1", 1, "alice", DateTimeOffset.UtcNow, null, CancellationToken.None);
Assert.Equal(PolicyActivationResultStatus.Activated, result.Status);
Assert.NotNull(result.Revision);
Assert.Equal(PolicyRevisionStatus.Active, result.Revision!.Status);
Assert.Single(result.Revision.Approvals);
}
[Fact]
public async Task ActivateRevision_WithTwoPersonRequirement_ReturnsPendingUntilSecondApproval()
{
await repository.CreateAsync("pack-2", "Pack", CancellationToken.None);
await repository.UpsertRevisionAsync("pack-2", 1, requiresTwoPersonApproval: true, PolicyRevisionStatus.Approved, CancellationToken.None);
var first = await repository.RecordActivationAsync("pack-2", 1, "alice", DateTimeOffset.UtcNow, null, CancellationToken.None);
Assert.Equal(PolicyActivationResultStatus.PendingSecondApproval, first.Status);
Assert.Equal(PolicyRevisionStatus.Approved, first.Revision!.Status);
Assert.Single(first.Revision.Approvals);
var duplicate = await repository.RecordActivationAsync("pack-2", 1, "alice", DateTimeOffset.UtcNow, null, CancellationToken.None);
Assert.Equal(PolicyActivationResultStatus.DuplicateApproval, duplicate.Status);
var second = await repository.RecordActivationAsync("pack-2", 1, "bob", DateTimeOffset.UtcNow, null, CancellationToken.None);
Assert.Equal(PolicyActivationResultStatus.Activated, second.Status);
Assert.Equal(PolicyRevisionStatus.Active, second.Revision!.Status);
Assert.Equal(2, second.Revision.Approvals.Length);
}
}

View File

@@ -1,212 +1,212 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using System.Net;
using System.Net.Http;
using System.Security.Claims;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;
using StellaOps.Auth.Client;
using StellaOps.Policy.Gateway.Clients;
using StellaOps.Policy.Gateway.Contracts;
using StellaOps.Policy.Gateway.Options;
using StellaOps.Policy.Gateway.Services;
using Xunit;
namespace StellaOps.Policy.Gateway.Tests;
public class PolicyEngineClientTests
{
[Fact]
public async Task ActivateRevision_UsesServiceTokenWhenForwardingContextMissing()
{
var options = CreateGatewayOptions();
options.PolicyEngine.ClientCredentials.Enabled = true;
options.PolicyEngine.ClientCredentials.ClientId = "policy-gateway";
options.PolicyEngine.ClientCredentials.ClientSecret = "secret";
options.PolicyEngine.ClientCredentials.Scopes.Clear();
options.PolicyEngine.ClientCredentials.Scopes.Add("policy:activate");
options.PolicyEngine.BaseAddress = "https://policy-engine.test/";
var optionsMonitor = new TestOptionsMonitor(options);
var tokenClient = new StubTokenClient();
var dpopGenerator = new PolicyGatewayDpopProofGenerator(new StubHostEnvironment(), optionsMonitor, TimeProvider.System, NullLogger<PolicyGatewayDpopProofGenerator>.Instance);
var tokenProvider = new PolicyEngineTokenProvider(tokenClient, optionsMonitor, dpopGenerator, TimeProvider.System, NullLogger<PolicyEngineTokenProvider>.Instance);
using var recordingHandler = new RecordingHandler();
using var httpClient = new HttpClient(recordingHandler)
{
BaseAddress = new Uri(options.PolicyEngine.BaseAddress)
};
var client = new PolicyEngineClient(httpClient, Microsoft.Extensions.Options.Options.Create(options), tokenProvider, NullLogger<PolicyEngineClient>.Instance);
var request = new ActivatePolicyRevisionRequest("comment");
var result = await client.ActivatePolicyRevisionAsync(null, "pack-123", 7, request, CancellationToken.None);
Assert.True(result.IsSuccess);
Assert.NotNull(recordingHandler.LastRequest);
var authorization = recordingHandler.LastRequest!.Headers.Authorization;
Assert.NotNull(authorization);
Assert.Equal("Bearer", authorization!.Scheme);
Assert.Equal("service-token", authorization.Parameter);
Assert.Equal(1, tokenClient.RequestCount);
}
[Fact]
public void Metrics_RecordActivation_EmitsExpectedTags()
{
using var metrics = new PolicyGatewayMetrics();
using var listener = new MeterListener();
var measurements = new List<(long Value, string Outcome, string Source)>();
var latencies = new List<(double Value, string Outcome, string Source)>();
listener.InstrumentPublished += (instrument, meterListener) =>
{
if (!string.Equals(instrument.Meter.Name, "StellaOps.Policy.Gateway", StringComparison.Ordinal))
{
return;
}
meterListener.EnableMeasurementEvents(instrument);
};
listener.SetMeasurementEventCallback<long>((instrument, value, tags, state) =>
{
if (instrument.Name != "policy_gateway_activation_requests_total")
{
return;
}
measurements.Add((value, GetTag(tags, "outcome"), GetTag(tags, "source")));
});
listener.SetMeasurementEventCallback<double>((instrument, value, tags, state) =>
{
if (instrument.Name != "policy_gateway_activation_latency_ms")
{
return;
}
latencies.Add((value, GetTag(tags, "outcome"), GetTag(tags, "source")));
});
listener.Start();
metrics.RecordActivation("activated", "service", 42.5);
listener.Dispose();
Assert.Contains(measurements, entry => entry.Value == 1 && entry.Outcome == "activated" && entry.Source == "service");
Assert.Contains(latencies, entry => entry.Outcome == "activated" && entry.Source == "service" && entry.Value == 42.5);
}
private static string GetTag(ReadOnlySpan<KeyValuePair<string, object?>> tags, string key)
{
foreach (var tag in tags)
{
if (string.Equals(tag.Key, key, StringComparison.Ordinal))
{
return tag.Value?.ToString() ?? string.Empty;
}
}
return string.Empty;
}
private static PolicyGatewayOptions CreateGatewayOptions()
{
return new PolicyGatewayOptions
{
PolicyEngine =
{
BaseAddress = "https://policy-engine.test/"
}
};
}
private sealed class TestOptionsMonitor : IOptionsMonitor<PolicyGatewayOptions>
{
public TestOptionsMonitor(PolicyGatewayOptions current)
{
CurrentValue = current;
}
public PolicyGatewayOptions CurrentValue { get; }
public PolicyGatewayOptions Get(string? name) => CurrentValue;
public IDisposable OnChange(Action<PolicyGatewayOptions, string?> listener) => EmptyDisposable.Instance;
private sealed class EmptyDisposable : IDisposable
{
public static readonly EmptyDisposable Instance = new();
public void Dispose()
{
}
}
}
private sealed class StubTokenClient : IStellaOpsTokenClient
{
public int RequestCount { get; private set; }
public IReadOnlyDictionary<string, string>? LastAdditionalParameters { get; private set; }
public Task<StellaOpsTokenResult> RequestClientCredentialsTokenAsync(string? scope = null, IReadOnlyDictionary<string, string>? additionalParameters = null, CancellationToken cancellationToken = default)
{
RequestCount++;
LastAdditionalParameters = additionalParameters;
return Task.FromResult(new StellaOpsTokenResult("service-token", "Bearer", DateTimeOffset.UtcNow.AddMinutes(5), Array.Empty<string>()));
}
public Task<StellaOpsTokenResult> RequestPasswordTokenAsync(string username, string password, string? scope = null, IReadOnlyDictionary<string, string>? additionalParameters = null, CancellationToken cancellationToken = default)
=> throw new NotSupportedException();
public Task<JsonWebKeySet> GetJsonWebKeySetAsync(CancellationToken cancellationToken = default)
=> throw new NotSupportedException();
public ValueTask<StellaOpsTokenCacheEntry?> GetCachedTokenAsync(string key, CancellationToken cancellationToken = default)
=> ValueTask.FromResult<StellaOpsTokenCacheEntry?>(null);
public ValueTask CacheTokenAsync(string key, StellaOpsTokenCacheEntry entry, CancellationToken cancellationToken = default)
=> ValueTask.CompletedTask;
public ValueTask ClearCachedTokenAsync(string key, CancellationToken cancellationToken = default)
=> ValueTask.CompletedTask;
}
private sealed class RecordingHandler : HttpMessageHandler
{
public HttpRequestMessage? LastRequest { get; private set; }
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
LastRequest = request;
var payload = JsonSerializer.Serialize(new PolicyRevisionActivationDto("activated", new PolicyRevisionDto(7, "Activated", false, DateTimeOffset.UtcNow, DateTimeOffset.UtcNow, Array.Empty<PolicyActivationApprovalDto>())));
var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(payload, Encoding.UTF8, "application/json")
};
return Task.FromResult(response);
}
}
private sealed class StubHostEnvironment : IHostEnvironment
{
public string EnvironmentName { get; set; } = "Development";
public string ApplicationName { get; set; } = "PolicyGatewayTests";
public string ContentRootPath { get; set; } = AppContext.BaseDirectory;
public IFileProvider ContentRootFileProvider { get; set; } = new NullFileProvider();
}
}
using System;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using System.Net;
using System.Net.Http;
using System.Security.Claims;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;
using StellaOps.Auth.Client;
using StellaOps.Policy.Gateway.Clients;
using StellaOps.Policy.Gateway.Contracts;
using StellaOps.Policy.Gateway.Options;
using StellaOps.Policy.Gateway.Services;
using Xunit;
namespace StellaOps.Policy.Gateway.Tests;
public class PolicyEngineClientTests
{
[Fact]
public async Task ActivateRevision_UsesServiceTokenWhenForwardingContextMissing()
{
var options = CreateGatewayOptions();
options.PolicyEngine.ClientCredentials.Enabled = true;
options.PolicyEngine.ClientCredentials.ClientId = "policy-gateway";
options.PolicyEngine.ClientCredentials.ClientSecret = "secret";
options.PolicyEngine.ClientCredentials.Scopes.Clear();
options.PolicyEngine.ClientCredentials.Scopes.Add("policy:activate");
options.PolicyEngine.BaseAddress = "https://policy-engine.test/";
var optionsMonitor = new TestOptionsMonitor(options);
var tokenClient = new StubTokenClient();
var dpopGenerator = new PolicyGatewayDpopProofGenerator(new StubHostEnvironment(), optionsMonitor, TimeProvider.System, NullLogger<PolicyGatewayDpopProofGenerator>.Instance);
var tokenProvider = new PolicyEngineTokenProvider(tokenClient, optionsMonitor, dpopGenerator, TimeProvider.System, NullLogger<PolicyEngineTokenProvider>.Instance);
using var recordingHandler = new RecordingHandler();
using var httpClient = new HttpClient(recordingHandler)
{
BaseAddress = new Uri(options.PolicyEngine.BaseAddress)
};
var client = new PolicyEngineClient(httpClient, Microsoft.Extensions.Options.Options.Create(options), tokenProvider, NullLogger<PolicyEngineClient>.Instance);
var request = new ActivatePolicyRevisionRequest("comment");
var result = await client.ActivatePolicyRevisionAsync(null, "pack-123", 7, request, CancellationToken.None);
Assert.True(result.IsSuccess);
Assert.NotNull(recordingHandler.LastRequest);
var authorization = recordingHandler.LastRequest!.Headers.Authorization;
Assert.NotNull(authorization);
Assert.Equal("Bearer", authorization!.Scheme);
Assert.Equal("service-token", authorization.Parameter);
Assert.Equal(1, tokenClient.RequestCount);
}
[Fact]
public void Metrics_RecordActivation_EmitsExpectedTags()
{
using var metrics = new PolicyGatewayMetrics();
using var listener = new MeterListener();
var measurements = new List<(long Value, string Outcome, string Source)>();
var latencies = new List<(double Value, string Outcome, string Source)>();
listener.InstrumentPublished += (instrument, meterListener) =>
{
if (!string.Equals(instrument.Meter.Name, "StellaOps.Policy.Gateway", StringComparison.Ordinal))
{
return;
}
meterListener.EnableMeasurementEvents(instrument);
};
listener.SetMeasurementEventCallback<long>((instrument, value, tags, state) =>
{
if (instrument.Name != "policy_gateway_activation_requests_total")
{
return;
}
measurements.Add((value, GetTag(tags, "outcome"), GetTag(tags, "source")));
});
listener.SetMeasurementEventCallback<double>((instrument, value, tags, state) =>
{
if (instrument.Name != "policy_gateway_activation_latency_ms")
{
return;
}
latencies.Add((value, GetTag(tags, "outcome"), GetTag(tags, "source")));
});
listener.Start();
metrics.RecordActivation("activated", "service", 42.5);
listener.Dispose();
Assert.Contains(measurements, entry => entry.Value == 1 && entry.Outcome == "activated" && entry.Source == "service");
Assert.Contains(latencies, entry => entry.Outcome == "activated" && entry.Source == "service" && entry.Value == 42.5);
}
private static string GetTag(ReadOnlySpan<KeyValuePair<string, object?>> tags, string key)
{
foreach (var tag in tags)
{
if (string.Equals(tag.Key, key, StringComparison.Ordinal))
{
return tag.Value?.ToString() ?? string.Empty;
}
}
return string.Empty;
}
private static PolicyGatewayOptions CreateGatewayOptions()
{
return new PolicyGatewayOptions
{
PolicyEngine =
{
BaseAddress = "https://policy-engine.test/"
}
};
}
private sealed class TestOptionsMonitor : IOptionsMonitor<PolicyGatewayOptions>
{
public TestOptionsMonitor(PolicyGatewayOptions current)
{
CurrentValue = current;
}
public PolicyGatewayOptions CurrentValue { get; }
public PolicyGatewayOptions Get(string? name) => CurrentValue;
public IDisposable OnChange(Action<PolicyGatewayOptions, string?> listener) => EmptyDisposable.Instance;
private sealed class EmptyDisposable : IDisposable
{
public static readonly EmptyDisposable Instance = new();
public void Dispose()
{
}
}
}
private sealed class StubTokenClient : IStellaOpsTokenClient
{
public int RequestCount { get; private set; }
public IReadOnlyDictionary<string, string>? LastAdditionalParameters { get; private set; }
public Task<StellaOpsTokenResult> RequestClientCredentialsTokenAsync(string? scope = null, IReadOnlyDictionary<string, string>? additionalParameters = null, CancellationToken cancellationToken = default)
{
RequestCount++;
LastAdditionalParameters = additionalParameters;
return Task.FromResult(new StellaOpsTokenResult("service-token", "Bearer", DateTimeOffset.UtcNow.AddMinutes(5), Array.Empty<string>()));
}
public Task<StellaOpsTokenResult> RequestPasswordTokenAsync(string username, string password, string? scope = null, IReadOnlyDictionary<string, string>? additionalParameters = null, CancellationToken cancellationToken = default)
=> throw new NotSupportedException();
public Task<JsonWebKeySet> GetJsonWebKeySetAsync(CancellationToken cancellationToken = default)
=> throw new NotSupportedException();
public ValueTask<StellaOpsTokenCacheEntry?> GetCachedTokenAsync(string key, CancellationToken cancellationToken = default)
=> ValueTask.FromResult<StellaOpsTokenCacheEntry?>(null);
public ValueTask CacheTokenAsync(string key, StellaOpsTokenCacheEntry entry, CancellationToken cancellationToken = default)
=> ValueTask.CompletedTask;
public ValueTask ClearCachedTokenAsync(string key, CancellationToken cancellationToken = default)
=> ValueTask.CompletedTask;
}
private sealed class RecordingHandler : HttpMessageHandler
{
public HttpRequestMessage? LastRequest { get; private set; }
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
LastRequest = request;
var payload = JsonSerializer.Serialize(new PolicyRevisionActivationDto("activated", new PolicyRevisionDto(7, "Activated", false, DateTimeOffset.UtcNow, DateTimeOffset.UtcNow, Array.Empty<PolicyActivationApprovalDto>())));
var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(payload, Encoding.UTF8, "application/json")
};
return Task.FromResult(response);
}
}
private sealed class StubHostEnvironment : IHostEnvironment
{
public string EnvironmentName { get; set; } = "Development";
public string ApplicationName { get; set; } = "PolicyGatewayTests";
public string ContentRootPath { get; set; } = AppContext.BaseDirectory;
public IFileProvider ContentRootFileProvider { get; set; } = new NullFileProvider();
}
}

View File

@@ -1,167 +1,167 @@
using System.Globalization;
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using StellaOps.Policy.Gateway.Options;
using StellaOps.Policy.Gateway.Services;
using Xunit;
namespace StellaOps.Policy.Gateway.Tests;
public sealed class PolicyGatewayDpopProofGeneratorTests
{
[Fact]
public void CreateProof_Throws_WhenDpopDisabled()
{
var options = CreateGatewayOptions();
options.PolicyEngine.Dpop.Enabled = false;
using var generator = new PolicyGatewayDpopProofGenerator(
new StubHostEnvironment(AppContext.BaseDirectory),
new TestOptionsMonitor(options),
TimeProvider.System,
NullLogger<PolicyGatewayDpopProofGenerator>.Instance);
var exception = Assert.Throws<InvalidOperationException>(() =>
generator.CreateProof(HttpMethod.Get, new Uri("https://policy-engine.example/api"), null));
Assert.Equal("DPoP proof requested while DPoP is disabled.", exception.Message);
}
[Fact]
public void CreateProof_Throws_WhenKeyFileMissing()
{
var tempRoot = Directory.CreateTempSubdirectory();
try
{
var options = CreateGatewayOptions();
options.PolicyEngine.Dpop.Enabled = true;
options.PolicyEngine.Dpop.KeyPath = "missing-key.pem";
using var generator = new PolicyGatewayDpopProofGenerator(
new StubHostEnvironment(tempRoot.FullName),
new TestOptionsMonitor(options),
TimeProvider.System,
NullLogger<PolicyGatewayDpopProofGenerator>.Instance);
var exception = Assert.Throws<FileNotFoundException>(() =>
generator.CreateProof(HttpMethod.Post, new Uri("https://policy-engine.example/token"), null));
Assert.Contains("missing-key.pem", exception.FileName, StringComparison.Ordinal);
}
finally
{
tempRoot.Delete(recursive: true);
}
}
[Fact]
public void CreateProof_UsesConfiguredAlgorithmAndEmbedsTokenHash()
{
var tempRoot = Directory.CreateTempSubdirectory();
try
{
var keyPath = CreateEcKey(tempRoot, ECCurve.NamedCurves.nistP384);
var options = CreateGatewayOptions();
options.PolicyEngine.Dpop.Enabled = true;
options.PolicyEngine.Dpop.KeyPath = keyPath;
options.PolicyEngine.Dpop.Algorithm = "ES384";
using var generator = new PolicyGatewayDpopProofGenerator(
new StubHostEnvironment(tempRoot.FullName),
new TestOptionsMonitor(options),
TimeProvider.System,
NullLogger<PolicyGatewayDpopProofGenerator>.Instance);
const string accessToken = "sample-access-token";
var proof = generator.CreateProof(HttpMethod.Delete, new Uri("https://policy-engine.example/api/resource"), accessToken);
var token = new JwtSecurityTokenHandler().ReadJwtToken(proof);
Assert.Equal("dpop+jwt", token.Header.Typ);
Assert.Equal("ES384", token.Header.Alg);
Assert.Equal("DELETE", token.Payload.TryGetValue("htm", out var method) ? method?.ToString() : null);
Assert.Equal("https://policy-engine.example/api/resource", token.Payload.TryGetValue("htu", out var uri) ? uri?.ToString() : null);
Assert.True(token.Payload.TryGetValue("iat", out var issuedAt));
Assert.True(long.TryParse(Convert.ToString(issuedAt, CultureInfo.InvariantCulture), out var epoch));
Assert.True(epoch > 0);
Assert.True(token.Payload.TryGetValue("jti", out var jti));
Assert.False(string.IsNullOrWhiteSpace(Convert.ToString(jti, CultureInfo.InvariantCulture)));
Assert.True(token.Payload.TryGetValue("ath", out var ath));
var expectedHash = Base64UrlEncoder.Encode(SHA256.HashData(Encoding.UTF8.GetBytes(accessToken)));
Assert.Equal(expectedHash, ath?.ToString());
}
finally
{
tempRoot.Delete(recursive: true);
}
}
private static PolicyGatewayOptions CreateGatewayOptions()
{
return new PolicyGatewayOptions
{
PolicyEngine =
{
BaseAddress = "https://policy-engine.example"
}
};
}
private static string CreateEcKey(DirectoryInfo directory, ECCurve curve)
{
using var ecdsa = ECDsa.Create(curve);
var privateKey = ecdsa.ExportPkcs8PrivateKey();
var pem = PemEncoding.Write("PRIVATE KEY", privateKey);
var path = Path.Combine(directory.FullName, "policy-gateway-dpop.pem");
File.WriteAllText(path, pem);
return path;
}
private sealed class StubHostEnvironment : IHostEnvironment
{
public StubHostEnvironment(string contentRootPath)
{
ContentRootPath = contentRootPath;
}
public string ApplicationName { get; set; } = "PolicyGatewayTests";
public IFileProvider ContentRootFileProvider { get; set; } = new NullFileProvider();
public string ContentRootPath { get; set; }
public string EnvironmentName { get; set; } = Environments.Development;
}
private sealed class TestOptionsMonitor : IOptionsMonitor<PolicyGatewayOptions>
{
public TestOptionsMonitor(PolicyGatewayOptions current)
{
CurrentValue = current;
}
public PolicyGatewayOptions CurrentValue { get; }
public PolicyGatewayOptions Get(string? name) => CurrentValue;
public IDisposable OnChange(Action<PolicyGatewayOptions, string?> listener) => EmptyDisposable.Instance;
private sealed class EmptyDisposable : IDisposable
{
public static readonly EmptyDisposable Instance = new();
public void Dispose()
{
}
}
}
}
using System.Globalization;
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using StellaOps.Policy.Gateway.Options;
using StellaOps.Policy.Gateway.Services;
using Xunit;
namespace StellaOps.Policy.Gateway.Tests;
public sealed class PolicyGatewayDpopProofGeneratorTests
{
[Fact]
public void CreateProof_Throws_WhenDpopDisabled()
{
var options = CreateGatewayOptions();
options.PolicyEngine.Dpop.Enabled = false;
using var generator = new PolicyGatewayDpopProofGenerator(
new StubHostEnvironment(AppContext.BaseDirectory),
new TestOptionsMonitor(options),
TimeProvider.System,
NullLogger<PolicyGatewayDpopProofGenerator>.Instance);
var exception = Assert.Throws<InvalidOperationException>(() =>
generator.CreateProof(HttpMethod.Get, new Uri("https://policy-engine.example/api"), null));
Assert.Equal("DPoP proof requested while DPoP is disabled.", exception.Message);
}
[Fact]
public void CreateProof_Throws_WhenKeyFileMissing()
{
var tempRoot = Directory.CreateTempSubdirectory();
try
{
var options = CreateGatewayOptions();
options.PolicyEngine.Dpop.Enabled = true;
options.PolicyEngine.Dpop.KeyPath = "missing-key.pem";
using var generator = new PolicyGatewayDpopProofGenerator(
new StubHostEnvironment(tempRoot.FullName),
new TestOptionsMonitor(options),
TimeProvider.System,
NullLogger<PolicyGatewayDpopProofGenerator>.Instance);
var exception = Assert.Throws<FileNotFoundException>(() =>
generator.CreateProof(HttpMethod.Post, new Uri("https://policy-engine.example/token"), null));
Assert.Contains("missing-key.pem", exception.FileName, StringComparison.Ordinal);
}
finally
{
tempRoot.Delete(recursive: true);
}
}
[Fact]
public void CreateProof_UsesConfiguredAlgorithmAndEmbedsTokenHash()
{
var tempRoot = Directory.CreateTempSubdirectory();
try
{
var keyPath = CreateEcKey(tempRoot, ECCurve.NamedCurves.nistP384);
var options = CreateGatewayOptions();
options.PolicyEngine.Dpop.Enabled = true;
options.PolicyEngine.Dpop.KeyPath = keyPath;
options.PolicyEngine.Dpop.Algorithm = "ES384";
using var generator = new PolicyGatewayDpopProofGenerator(
new StubHostEnvironment(tempRoot.FullName),
new TestOptionsMonitor(options),
TimeProvider.System,
NullLogger<PolicyGatewayDpopProofGenerator>.Instance);
const string accessToken = "sample-access-token";
var proof = generator.CreateProof(HttpMethod.Delete, new Uri("https://policy-engine.example/api/resource"), accessToken);
var token = new JwtSecurityTokenHandler().ReadJwtToken(proof);
Assert.Equal("dpop+jwt", token.Header.Typ);
Assert.Equal("ES384", token.Header.Alg);
Assert.Equal("DELETE", token.Payload.TryGetValue("htm", out var method) ? method?.ToString() : null);
Assert.Equal("https://policy-engine.example/api/resource", token.Payload.TryGetValue("htu", out var uri) ? uri?.ToString() : null);
Assert.True(token.Payload.TryGetValue("iat", out var issuedAt));
Assert.True(long.TryParse(Convert.ToString(issuedAt, CultureInfo.InvariantCulture), out var epoch));
Assert.True(epoch > 0);
Assert.True(token.Payload.TryGetValue("jti", out var jti));
Assert.False(string.IsNullOrWhiteSpace(Convert.ToString(jti, CultureInfo.InvariantCulture)));
Assert.True(token.Payload.TryGetValue("ath", out var ath));
var expectedHash = Base64UrlEncoder.Encode(SHA256.HashData(Encoding.UTF8.GetBytes(accessToken)));
Assert.Equal(expectedHash, ath?.ToString());
}
finally
{
tempRoot.Delete(recursive: true);
}
}
private static PolicyGatewayOptions CreateGatewayOptions()
{
return new PolicyGatewayOptions
{
PolicyEngine =
{
BaseAddress = "https://policy-engine.example"
}
};
}
private static string CreateEcKey(DirectoryInfo directory, ECCurve curve)
{
using var ecdsa = ECDsa.Create(curve);
var privateKey = ecdsa.ExportPkcs8PrivateKey();
var pem = PemEncoding.Write("PRIVATE KEY", privateKey);
var path = Path.Combine(directory.FullName, "policy-gateway-dpop.pem");
File.WriteAllText(path, pem);
return path;
}
private sealed class StubHostEnvironment : IHostEnvironment
{
public StubHostEnvironment(string contentRootPath)
{
ContentRootPath = contentRootPath;
}
public string ApplicationName { get; set; } = "PolicyGatewayTests";
public IFileProvider ContentRootFileProvider { get; set; } = new NullFileProvider();
public string ContentRootPath { get; set; }
public string EnvironmentName { get; set; } = Environments.Development;
}
private sealed class TestOptionsMonitor : IOptionsMonitor<PolicyGatewayOptions>
{
public TestOptionsMonitor(PolicyGatewayOptions current)
{
CurrentValue = current;
}
public PolicyGatewayOptions CurrentValue { get; }
public PolicyGatewayOptions Get(string? name) => CurrentValue;
public IDisposable OnChange(Action<PolicyGatewayOptions, string?> listener) => EmptyDisposable.Instance;
private sealed class EmptyDisposable : IDisposable
{
public static readonly EmptyDisposable Instance = new();
public void Dispose()
{
}
}
}
}

View File

@@ -1,32 +1,32 @@
using System;
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
namespace StellaOps.Policy.Tests;
public sealed class PolicyBinderTests
{
[Fact]
public void Bind_ValidYaml_ReturnsSuccess()
{
const string yaml = """
version: "1.0"
rules:
- name: Block Critical
severity: [Critical]
sources: [NVD]
action: block
""";
var result = PolicyBinder.Bind(yaml, PolicyDocumentFormat.Yaml);
Assert.True(result.Success);
Assert.Equal("1.0", result.Document.Version);
Assert.Single(result.Document.Rules);
Assert.Empty(result.Issues);
namespace StellaOps.Policy.Tests;
public sealed class PolicyBinderTests
{
[Fact]
public void Bind_ValidYaml_ReturnsSuccess()
{
const string yaml = """
version: "1.0"
rules:
- name: Block Critical
severity: [Critical]
sources: [NVD]
action: block
""";
var result = PolicyBinder.Bind(yaml, PolicyDocumentFormat.Yaml);
Assert.True(result.Success);
Assert.Equal("1.0", result.Document.Version);
Assert.Single(result.Document.Rules);
Assert.Empty(result.Issues);
}
[Fact]
@@ -99,59 +99,59 @@ public sealed class PolicyBinderTests
Assert.Contains(result.Issues, issue => issue.Code == "policy.exceptions.effect.downgrade.missingSeverity");
}
[Fact]
public void Bind_InvalidSeverity_ReturnsError()
{
const string yaml = """
version: "1.0"
rules:
- name: Invalid Severity
severity: [Nope]
action: block
""";
var result = PolicyBinder.Bind(yaml, PolicyDocumentFormat.Yaml);
Assert.False(result.Success);
Assert.Contains(result.Issues, issue => issue.Code == "policy.severity.invalid");
}
[Fact]
public async Task Cli_StrictMode_FailsOnWarnings()
{
const string yaml = """
version: "1.0"
rules:
- name: Quiet Warning
sources: ["", "NVD"]
action: ignore
""";
var path = Path.Combine(Path.GetTempPath(), $"policy-{Guid.NewGuid():N}.yaml");
await File.WriteAllTextAsync(path, yaml);
try
{
using var output = new StringWriter();
using var error = new StringWriter();
var cli = new PolicyValidationCli(output, error);
var options = new PolicyValidationCliOptions
{
Inputs = new[] { path },
Strict = true,
};
var exitCode = await cli.RunAsync(options, CancellationToken.None);
Assert.Equal(2, exitCode);
Assert.Contains("WARNING", output.ToString());
}
finally
{
if (File.Exists(path))
{
File.Delete(path);
}
}
}
}
[Fact]
public void Bind_InvalidSeverity_ReturnsError()
{
const string yaml = """
version: "1.0"
rules:
- name: Invalid Severity
severity: [Nope]
action: block
""";
var result = PolicyBinder.Bind(yaml, PolicyDocumentFormat.Yaml);
Assert.False(result.Success);
Assert.Contains(result.Issues, issue => issue.Code == "policy.severity.invalid");
}
[Fact]
public async Task Cli_StrictMode_FailsOnWarnings()
{
const string yaml = """
version: "1.0"
rules:
- name: Quiet Warning
sources: ["", "NVD"]
action: ignore
""";
var path = Path.Combine(Path.GetTempPath(), $"policy-{Guid.NewGuid():N}.yaml");
await File.WriteAllTextAsync(path, yaml);
try
{
using var output = new StringWriter();
using var error = new StringWriter();
var cli = new PolicyValidationCli(output, error);
var options = new PolicyValidationCliOptions
{
Inputs = new[] { path },
Strict = true,
};
var exitCode = await cli.RunAsync(options, CancellationToken.None);
Assert.Equal(2, exitCode);
Assert.Contains("WARNING", output.ToString());
}
finally
{
if (File.Exists(path))
{
File.Delete(path);
}
}
}
}

View File

@@ -1,41 +1,41 @@
using System.Collections.Immutable;
using Xunit;
namespace StellaOps.Policy.Tests;
public sealed class PolicyEvaluationTests
{
[Fact]
public void EvaluateFinding_AppliesTrustAndReachabilityWeights()
{
var action = new PolicyAction(PolicyActionType.Block, null, null, null, false);
var rule = PolicyRule.Create(
"BlockMedium",
action,
ImmutableArray.Create(PolicySeverity.Medium),
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
PolicyRuleMatchCriteria.Empty,
expires: null,
justification: null);
using System.Collections.Immutable;
using Xunit;
namespace StellaOps.Policy.Tests;
public sealed class PolicyEvaluationTests
{
[Fact]
public void EvaluateFinding_AppliesTrustAndReachabilityWeights()
{
var action = new PolicyAction(PolicyActionType.Block, null, null, null, false);
var rule = PolicyRule.Create(
"BlockMedium",
action,
ImmutableArray.Create(PolicySeverity.Medium),
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
PolicyRuleMatchCriteria.Empty,
expires: null,
justification: null);
var document = new PolicyDocument(
PolicySchema.CurrentVersion,
ImmutableArray.Create(rule),
ImmutableDictionary<string, string>.Empty,
PolicyExceptionConfiguration.Empty);
var config = PolicyScoringConfig.Default;
var finding = PolicyFinding.Create(
"finding-medium",
PolicySeverity.Medium,
source: "community",
tags: ImmutableArray.Create("reachability:indirect"));
var config = PolicyScoringConfig.Default;
var finding = PolicyFinding.Create(
"finding-medium",
PolicySeverity.Medium,
source: "community",
tags: ImmutableArray.Create("reachability:indirect"));
var verdict = PolicyEvaluation.EvaluateFinding(document, config, finding, out var explanation);
Assert.Equal(PolicyVerdictStatus.Blocked, verdict.Status);
Assert.Equal(19.5, verdict.Score, 3);
@@ -48,43 +48,43 @@ public sealed class PolicyEvaluationTests
Assert.NotNull(explanation);
Assert.Equal(PolicyVerdictStatus.Blocked, explanation!.Decision);
Assert.Equal("BlockMedium", explanation.RuleName);
}
[Fact]
public void EvaluateFinding_QuietWithRequireVexAppliesQuietPenalty()
{
var ignoreOptions = new PolicyIgnoreOptions(null, null);
var requireVexOptions = new PolicyRequireVexOptions(
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty);
var action = new PolicyAction(PolicyActionType.Ignore, ignoreOptions, null, requireVexOptions, true);
var rule = PolicyRule.Create(
"QuietIgnore",
action,
ImmutableArray.Create(PolicySeverity.Critical),
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
PolicyRuleMatchCriteria.Empty,
expires: null,
justification: null);
}
[Fact]
public void EvaluateFinding_QuietWithRequireVexAppliesQuietPenalty()
{
var ignoreOptions = new PolicyIgnoreOptions(null, null);
var requireVexOptions = new PolicyRequireVexOptions(
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty);
var action = new PolicyAction(PolicyActionType.Ignore, ignoreOptions, null, requireVexOptions, true);
var rule = PolicyRule.Create(
"QuietIgnore",
action,
ImmutableArray.Create(PolicySeverity.Critical),
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
PolicyRuleMatchCriteria.Empty,
expires: null,
justification: null);
var document = new PolicyDocument(
PolicySchema.CurrentVersion,
ImmutableArray.Create(rule),
ImmutableDictionary<string, string>.Empty,
PolicyExceptionConfiguration.Empty);
var config = PolicyScoringConfig.Default;
var finding = PolicyFinding.Create(
"finding-critical",
PolicySeverity.Critical,
tags: ImmutableArray.Create("reachability:entrypoint"));
var config = PolicyScoringConfig.Default;
var finding = PolicyFinding.Create(
"finding-critical",
PolicySeverity.Critical,
tags: ImmutableArray.Create("reachability:entrypoint"));
var verdict = PolicyEvaluation.EvaluateFinding(document, config, finding, out var explanation);
Assert.Equal(PolicyVerdictStatus.Ignored, verdict.Status);
Assert.True(verdict.Quiet);
Assert.Equal("QuietIgnore", verdict.QuietedBy);
@@ -97,39 +97,39 @@ public sealed class PolicyEvaluationTests
Assert.NotNull(explanation);
Assert.Equal(PolicyVerdictStatus.Ignored, explanation!.Decision);
}
[Fact]
public void EvaluateFinding_UnknownSeverityComputesConfidence()
{
var action = new PolicyAction(PolicyActionType.Block, null, null, null, false);
var rule = PolicyRule.Create(
"BlockUnknown",
action,
ImmutableArray.Create(PolicySeverity.Unknown),
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
PolicyRuleMatchCriteria.Empty,
expires: null,
justification: null);
}
[Fact]
public void EvaluateFinding_UnknownSeverityComputesConfidence()
{
var action = new PolicyAction(PolicyActionType.Block, null, null, null, false);
var rule = PolicyRule.Create(
"BlockUnknown",
action,
ImmutableArray.Create(PolicySeverity.Unknown),
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
PolicyRuleMatchCriteria.Empty,
expires: null,
justification: null);
var document = new PolicyDocument(
PolicySchema.CurrentVersion,
ImmutableArray.Create(rule),
ImmutableDictionary<string, string>.Empty,
PolicyExceptionConfiguration.Empty);
var config = PolicyScoringConfig.Default;
var finding = PolicyFinding.Create(
"finding-unknown",
PolicySeverity.Unknown,
tags: ImmutableArray.Create("reachability:unknown", "unknown-age-days:5"));
var config = PolicyScoringConfig.Default;
var finding = PolicyFinding.Create(
"finding-unknown",
PolicySeverity.Unknown,
tags: ImmutableArray.Create("reachability:unknown", "unknown-age-days:5"));
var verdict = PolicyEvaluation.EvaluateFinding(document, config, finding, out var explanation);
Assert.Equal(PolicyVerdictStatus.Blocked, verdict.Status);
Assert.Equal(30, verdict.Score, 3); // 60 * 1 * 0.5
Assert.Equal(0.55, verdict.UnknownConfidence ?? 0, 3);

View File

@@ -1,185 +1,185 @@
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Time.Testing;
using Xunit;
using Xunit.Abstractions;
namespace StellaOps.Policy.Tests;
public sealed class PolicyPreviewServiceTests
{
private readonly ITestOutputHelper _output;
public PolicyPreviewServiceTests(ITestOutputHelper output)
{
_output = output ?? throw new ArgumentNullException(nameof(output));
}
[Fact]
public async Task PreviewAsync_ComputesDiffs_ForBlockingRule()
{
const string yaml = """
version: "1.0"
rules:
- name: Block Critical
severity: [Critical]
action: block
""";
var snapshotRepo = new InMemoryPolicySnapshotRepository();
var auditRepo = new InMemoryPolicyAuditRepository();
var timeProvider = new FakeTimeProvider();
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, timeProvider, NullLogger<PolicySnapshotStore>.Instance);
await store.SaveAsync(new PolicySnapshotContent(yaml, PolicyDocumentFormat.Yaml, "tester", null, null), CancellationToken.None);
var service = new PolicyPreviewService(store, NullLogger<PolicyPreviewService>.Instance);
var findings = ImmutableArray.Create(
PolicyFinding.Create("finding-1", PolicySeverity.Critical, environment: "prod", source: "NVD"),
PolicyFinding.Create("finding-2", PolicySeverity.Low));
var baseline = ImmutableArray.Create(
new PolicyVerdict("finding-1", PolicyVerdictStatus.Pass),
new PolicyVerdict("finding-2", PolicyVerdictStatus.Pass));
var response = await service.PreviewAsync(new PolicyPreviewRequest(
"sha256:abc",
findings,
baseline),
CancellationToken.None);
Assert.True(response.Success);
Assert.Equal(1, response.ChangedCount);
var diff1 = Assert.Single(response.Diffs.Where(diff => diff.Projected.FindingId == "finding-1"));
Assert.Equal(PolicyVerdictStatus.Pass, diff1.Baseline.Status);
Assert.Equal(PolicyVerdictStatus.Blocked, diff1.Projected.Status);
Assert.Equal("Block Critical", diff1.Projected.RuleName);
Assert.True(diff1.Projected.Score > 0);
Assert.Equal(PolicyScoringConfig.Default.Version, diff1.Projected.ConfigVersion);
Assert.Equal(PolicyVerdictStatus.Pass, response.Diffs.First(diff => diff.Projected.FindingId == "finding-2").Projected.Status);
}
[Fact]
public async Task PreviewAsync_UsesProposedPolicy_WhenProvided()
{
const string yaml = """
version: "1.0"
rules:
- name: Ignore Dev
environments: [dev]
action:
type: ignore
justification: dev waiver
""";
var snapshotRepo = new InMemoryPolicySnapshotRepository();
var auditRepo = new InMemoryPolicyAuditRepository();
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, TimeProvider.System, NullLogger<PolicySnapshotStore>.Instance);
var service = new PolicyPreviewService(store, NullLogger<PolicyPreviewService>.Instance);
var findings = ImmutableArray.Create(
PolicyFinding.Create("finding-1", PolicySeverity.Medium, environment: "dev"));
var baseline = ImmutableArray.Create(new PolicyVerdict("finding-1", PolicyVerdictStatus.Blocked));
var response = await service.PreviewAsync(new PolicyPreviewRequest(
"sha256:def",
findings,
baseline,
SnapshotOverride: null,
ProposedPolicy: new PolicySnapshotContent(yaml, PolicyDocumentFormat.Yaml, "tester", null, "dev override")),
CancellationToken.None);
Assert.True(response.Success);
var diff = Assert.Single(response.Diffs);
Assert.Equal(PolicyVerdictStatus.Blocked, diff.Baseline.Status);
Assert.Equal(PolicyVerdictStatus.Ignored, diff.Projected.Status);
Assert.Equal("Ignore Dev", diff.Projected.RuleName);
Assert.True(diff.Projected.Score >= 0);
Assert.Equal(1, response.ChangedCount);
}
[Fact]
public async Task PreviewAsync_ReturnsIssues_WhenPolicyInvalid()
{
var snapshotRepo = new InMemoryPolicySnapshotRepository();
var auditRepo = new InMemoryPolicyAuditRepository();
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, TimeProvider.System, NullLogger<PolicySnapshotStore>.Instance);
var service = new PolicyPreviewService(store, NullLogger<PolicyPreviewService>.Instance);
const string invalid = "version: 1.0";
var request = new PolicyPreviewRequest(
"sha256:ghi",
ImmutableArray<PolicyFinding>.Empty,
ImmutableArray<PolicyVerdict>.Empty,
SnapshotOverride: null,
ProposedPolicy: new PolicySnapshotContent(invalid, PolicyDocumentFormat.Yaml, null, null, null));
var response = await service.PreviewAsync(request, CancellationToken.None);
Assert.False(response.Success);
Assert.NotEmpty(response.Issues);
}
[Fact]
public async Task PreviewAsync_QuietWithoutVexDowngradesToWarn()
{
const string yaml = """
version: "1.0"
rules:
- name: Quiet Without VEX
severity: [Low]
quiet: true
action:
type: ignore
""";
var binding = PolicyBinder.Bind(yaml, PolicyDocumentFormat.Yaml);
if (!binding.Success)
{
foreach (var issue in binding.Issues)
{
_output.WriteLine($"{issue.Severity} {issue.Code} {issue.Path} :: {issue.Message}");
}
var parseMethod = typeof(PolicyBinder).GetMethod("ParseToNode", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
var node = (System.Text.Json.Nodes.JsonNode?)parseMethod?.Invoke(null, new object[] { yaml, PolicyDocumentFormat.Yaml });
_output.WriteLine(node?.ToJsonString() ?? "<null>");
}
Assert.True(binding.Success);
Assert.Empty(binding.Issues);
Assert.False(binding.Document.Rules[0].Metadata.ContainsKey("quiet"));
Assert.True(binding.Document.Rules[0].Action.Quiet);
var store = new PolicySnapshotStore(new InMemoryPolicySnapshotRepository(), new InMemoryPolicyAuditRepository(), TimeProvider.System, NullLogger<PolicySnapshotStore>.Instance);
await store.SaveAsync(new PolicySnapshotContent(yaml, PolicyDocumentFormat.Yaml, "tester", null, "quiet test"), CancellationToken.None);
var snapshot = await store.GetLatestAsync();
Assert.NotNull(snapshot);
Assert.True(snapshot!.Document.Rules[0].Action.Quiet);
Assert.Null(snapshot.Document.Rules[0].Action.RequireVex);
Assert.Equal(PolicyActionType.Ignore, snapshot.Document.Rules[0].Action.Type);
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Time.Testing;
using Xunit;
using Xunit.Abstractions;
namespace StellaOps.Policy.Tests;
public sealed class PolicyPreviewServiceTests
{
private readonly ITestOutputHelper _output;
public PolicyPreviewServiceTests(ITestOutputHelper output)
{
_output = output ?? throw new ArgumentNullException(nameof(output));
}
[Fact]
public async Task PreviewAsync_ComputesDiffs_ForBlockingRule()
{
const string yaml = """
version: "1.0"
rules:
- name: Block Critical
severity: [Critical]
action: block
""";
var snapshotRepo = new InMemoryPolicySnapshotRepository();
var auditRepo = new InMemoryPolicyAuditRepository();
var timeProvider = new FakeTimeProvider();
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, timeProvider, NullLogger<PolicySnapshotStore>.Instance);
await store.SaveAsync(new PolicySnapshotContent(yaml, PolicyDocumentFormat.Yaml, "tester", null, null), CancellationToken.None);
var service = new PolicyPreviewService(store, NullLogger<PolicyPreviewService>.Instance);
var findings = ImmutableArray.Create(
PolicyFinding.Create("finding-1", PolicySeverity.Critical, environment: "prod", source: "NVD"),
PolicyFinding.Create("finding-2", PolicySeverity.Low));
var baseline = ImmutableArray.Create(
new PolicyVerdict("finding-1", PolicyVerdictStatus.Pass),
new PolicyVerdict("finding-2", PolicyVerdictStatus.Pass));
var response = await service.PreviewAsync(new PolicyPreviewRequest(
"sha256:abc",
findings,
baseline),
CancellationToken.None);
Assert.True(response.Success);
Assert.Equal(1, response.ChangedCount);
var diff1 = Assert.Single(response.Diffs.Where(diff => diff.Projected.FindingId == "finding-1"));
Assert.Equal(PolicyVerdictStatus.Pass, diff1.Baseline.Status);
Assert.Equal(PolicyVerdictStatus.Blocked, diff1.Projected.Status);
Assert.Equal("Block Critical", diff1.Projected.RuleName);
Assert.True(diff1.Projected.Score > 0);
Assert.Equal(PolicyScoringConfig.Default.Version, diff1.Projected.ConfigVersion);
Assert.Equal(PolicyVerdictStatus.Pass, response.Diffs.First(diff => diff.Projected.FindingId == "finding-2").Projected.Status);
}
[Fact]
public async Task PreviewAsync_UsesProposedPolicy_WhenProvided()
{
const string yaml = """
version: "1.0"
rules:
- name: Ignore Dev
environments: [dev]
action:
type: ignore
justification: dev waiver
""";
var snapshotRepo = new InMemoryPolicySnapshotRepository();
var auditRepo = new InMemoryPolicyAuditRepository();
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, TimeProvider.System, NullLogger<PolicySnapshotStore>.Instance);
var service = new PolicyPreviewService(store, NullLogger<PolicyPreviewService>.Instance);
var findings = ImmutableArray.Create(
PolicyFinding.Create("finding-1", PolicySeverity.Medium, environment: "dev"));
var baseline = ImmutableArray.Create(new PolicyVerdict("finding-1", PolicyVerdictStatus.Blocked));
var response = await service.PreviewAsync(new PolicyPreviewRequest(
"sha256:def",
findings,
baseline,
SnapshotOverride: null,
ProposedPolicy: new PolicySnapshotContent(yaml, PolicyDocumentFormat.Yaml, "tester", null, "dev override")),
CancellationToken.None);
Assert.True(response.Success);
var diff = Assert.Single(response.Diffs);
Assert.Equal(PolicyVerdictStatus.Blocked, diff.Baseline.Status);
Assert.Equal(PolicyVerdictStatus.Ignored, diff.Projected.Status);
Assert.Equal("Ignore Dev", diff.Projected.RuleName);
Assert.True(diff.Projected.Score >= 0);
Assert.Equal(1, response.ChangedCount);
}
[Fact]
public async Task PreviewAsync_ReturnsIssues_WhenPolicyInvalid()
{
var snapshotRepo = new InMemoryPolicySnapshotRepository();
var auditRepo = new InMemoryPolicyAuditRepository();
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, TimeProvider.System, NullLogger<PolicySnapshotStore>.Instance);
var service = new PolicyPreviewService(store, NullLogger<PolicyPreviewService>.Instance);
const string invalid = "version: 1.0";
var request = new PolicyPreviewRequest(
"sha256:ghi",
ImmutableArray<PolicyFinding>.Empty,
ImmutableArray<PolicyVerdict>.Empty,
SnapshotOverride: null,
ProposedPolicy: new PolicySnapshotContent(invalid, PolicyDocumentFormat.Yaml, null, null, null));
var response = await service.PreviewAsync(request, CancellationToken.None);
Assert.False(response.Success);
Assert.NotEmpty(response.Issues);
}
[Fact]
public async Task PreviewAsync_QuietWithoutVexDowngradesToWarn()
{
const string yaml = """
version: "1.0"
rules:
- name: Quiet Without VEX
severity: [Low]
quiet: true
action:
type: ignore
""";
var binding = PolicyBinder.Bind(yaml, PolicyDocumentFormat.Yaml);
if (!binding.Success)
{
foreach (var issue in binding.Issues)
{
_output.WriteLine($"{issue.Severity} {issue.Code} {issue.Path} :: {issue.Message}");
}
var parseMethod = typeof(PolicyBinder).GetMethod("ParseToNode", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
var node = (System.Text.Json.Nodes.JsonNode?)parseMethod?.Invoke(null, new object[] { yaml, PolicyDocumentFormat.Yaml });
_output.WriteLine(node?.ToJsonString() ?? "<null>");
}
Assert.True(binding.Success);
Assert.Empty(binding.Issues);
Assert.False(binding.Document.Rules[0].Metadata.ContainsKey("quiet"));
Assert.True(binding.Document.Rules[0].Action.Quiet);
var store = new PolicySnapshotStore(new InMemoryPolicySnapshotRepository(), new InMemoryPolicyAuditRepository(), TimeProvider.System, NullLogger<PolicySnapshotStore>.Instance);
await store.SaveAsync(new PolicySnapshotContent(yaml, PolicyDocumentFormat.Yaml, "tester", null, "quiet test"), CancellationToken.None);
var snapshot = await store.GetLatestAsync();
Assert.NotNull(snapshot);
Assert.True(snapshot!.Document.Rules[0].Action.Quiet);
Assert.Null(snapshot.Document.Rules[0].Action.RequireVex);
Assert.Equal(PolicyActionType.Ignore, snapshot.Document.Rules[0].Action.Type);
var manualVerdict = PolicyEvaluation.EvaluateFinding(snapshot.Document, snapshot.ScoringConfig, PolicyFinding.Create("finding-quiet", PolicySeverity.Low), out _);
Assert.Equal(PolicyVerdictStatus.Warned, manualVerdict.Status);
var service = new PolicyPreviewService(store, NullLogger<PolicyPreviewService>.Instance);
var findings = ImmutableArray.Create(PolicyFinding.Create("finding-quiet", PolicySeverity.Low));
var baseline = ImmutableArray<PolicyVerdict>.Empty;
var response = await service.PreviewAsync(new PolicyPreviewRequest(
"sha256:quiet",
findings,
baseline),
CancellationToken.None);
Assert.True(response.Success);
var verdict = Assert.Single(response.Diffs).Projected;
Assert.Equal(PolicyVerdictStatus.Warned, verdict.Status);
Assert.Contains("requireVex", verdict.Notes, System.StringComparison.OrdinalIgnoreCase);
Assert.True(verdict.Score >= 0);
}
}
Assert.Equal(PolicyVerdictStatus.Warned, manualVerdict.Status);
var service = new PolicyPreviewService(store, NullLogger<PolicyPreviewService>.Instance);
var findings = ImmutableArray.Create(PolicyFinding.Create("finding-quiet", PolicySeverity.Low));
var baseline = ImmutableArray<PolicyVerdict>.Empty;
var response = await service.PreviewAsync(new PolicyPreviewRequest(
"sha256:quiet",
findings,
baseline),
CancellationToken.None);
Assert.True(response.Success);
var verdict = Assert.Single(response.Diffs).Projected;
Assert.Equal(PolicyVerdictStatus.Warned, verdict.Status);
Assert.Contains("requireVex", verdict.Notes, System.StringComparison.OrdinalIgnoreCase);
Assert.True(verdict.Score >= 0);
}
}

View File

@@ -1,66 +1,66 @@
using System;
using System.IO;
using Xunit;
namespace StellaOps.Policy.Tests;
public sealed class PolicyScoringConfigTests
{
[Fact]
public void LoadDefaultReturnsConfig()
{
var config = PolicyScoringConfigBinder.LoadDefault();
Assert.NotNull(config);
Assert.Equal("1.0", config.Version);
Assert.NotEmpty(config.SeverityWeights);
Assert.True(config.SeverityWeights.ContainsKey(PolicySeverity.Critical));
Assert.True(config.QuietPenalty > 0);
Assert.NotEmpty(config.ReachabilityBuckets);
Assert.Contains("entrypoint", config.ReachabilityBuckets.Keys);
Assert.False(config.UnknownConfidence.Bands.IsDefaultOrEmpty);
Assert.Equal("high", config.UnknownConfidence.Bands[0].Name);
}
[Fact]
public void BindRejectsEmptyContent()
{
var result = PolicyScoringConfigBinder.Bind(string.Empty, PolicyDocumentFormat.Json);
Assert.False(result.Success);
Assert.NotEmpty(result.Issues);
}
[Fact]
public void BindRejectsInvalidSchema()
{
const string json = """
{
"version": "1.0",
"severityWeights": {
"Critical": 90.0
}
}
""";
var result = PolicyScoringConfigBinder.Bind(json, PolicyDocumentFormat.Json);
Assert.False(result.Success);
Assert.Contains(result.Issues, issue => issue.Code.StartsWith("scoring.schema", StringComparison.OrdinalIgnoreCase));
Assert.Null(result.Config);
}
[Fact]
public void DefaultResourceDigestMatchesGolden()
{
var assembly = typeof(PolicyScoringConfig).Assembly;
using var stream = assembly.GetManifestResourceStream("StellaOps.Policy.Schemas.policy-scoring-default.json")
?? throw new InvalidOperationException("Unable to locate embedded scoring default resource.");
using var reader = new StreamReader(stream);
var json = reader.ReadToEnd();
var binding = PolicyScoringConfigBinder.Bind(json, PolicyDocumentFormat.Json);
Assert.True(binding.Success);
Assert.NotNull(binding.Config);
var digest = PolicyScoringConfigDigest.Compute(binding.Config!);
Assert.Equal("5ef2e43a112cb00753beb7811dd2e1720f2385e2289d0fb6abcf7bbbb8cda2d2", digest);
}
}
using System;
using System.IO;
using Xunit;
namespace StellaOps.Policy.Tests;
public sealed class PolicyScoringConfigTests
{
[Fact]
public void LoadDefaultReturnsConfig()
{
var config = PolicyScoringConfigBinder.LoadDefault();
Assert.NotNull(config);
Assert.Equal("1.0", config.Version);
Assert.NotEmpty(config.SeverityWeights);
Assert.True(config.SeverityWeights.ContainsKey(PolicySeverity.Critical));
Assert.True(config.QuietPenalty > 0);
Assert.NotEmpty(config.ReachabilityBuckets);
Assert.Contains("entrypoint", config.ReachabilityBuckets.Keys);
Assert.False(config.UnknownConfidence.Bands.IsDefaultOrEmpty);
Assert.Equal("high", config.UnknownConfidence.Bands[0].Name);
}
[Fact]
public void BindRejectsEmptyContent()
{
var result = PolicyScoringConfigBinder.Bind(string.Empty, PolicyDocumentFormat.Json);
Assert.False(result.Success);
Assert.NotEmpty(result.Issues);
}
[Fact]
public void BindRejectsInvalidSchema()
{
const string json = """
{
"version": "1.0",
"severityWeights": {
"Critical": 90.0
}
}
""";
var result = PolicyScoringConfigBinder.Bind(json, PolicyDocumentFormat.Json);
Assert.False(result.Success);
Assert.Contains(result.Issues, issue => issue.Code.StartsWith("scoring.schema", StringComparison.OrdinalIgnoreCase));
Assert.Null(result.Config);
}
[Fact]
public void DefaultResourceDigestMatchesGolden()
{
var assembly = typeof(PolicyScoringConfig).Assembly;
using var stream = assembly.GetManifestResourceStream("StellaOps.Policy.Schemas.policy-scoring-default.json")
?? throw new InvalidOperationException("Unable to locate embedded scoring default resource.");
using var reader = new StreamReader(stream);
var json = reader.ReadToEnd();
var binding = PolicyScoringConfigBinder.Bind(json, PolicyDocumentFormat.Json);
Assert.True(binding.Success);
Assert.NotNull(binding.Config);
var digest = PolicyScoringConfigDigest.Compute(binding.Config!);
Assert.Equal("5ef2e43a112cb00753beb7811dd2e1720f2385e2289d0fb6abcf7bbbb8cda2d2", digest);
}
}

View File

@@ -1,94 +1,94 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Time.Testing;
using Xunit;
namespace StellaOps.Policy.Tests;
public sealed class PolicySnapshotStoreTests
{
private const string BasePolicyYaml = """
version: "1.0"
rules:
- name: Block Critical
severity: [Critical]
action: block
""";
[Fact]
public async Task SaveAsync_CreatesNewSnapshotAndAuditEntry()
{
var snapshotRepo = new InMemoryPolicySnapshotRepository();
var auditRepo = new InMemoryPolicyAuditRepository();
var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 18, 10, 0, 0, TimeSpan.Zero));
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, timeProvider, NullLogger<PolicySnapshotStore>.Instance);
var content = new PolicySnapshotContent(BasePolicyYaml, PolicyDocumentFormat.Yaml, "cli", "test", null);
var result = await store.SaveAsync(content, CancellationToken.None);
Assert.True(result.Success);
Assert.True(result.Created);
Assert.NotNull(result.Snapshot);
Assert.Equal("rev-1", result.Snapshot!.RevisionId);
Assert.Equal(result.Digest, result.Snapshot.Digest);
Assert.Equal(timeProvider.GetUtcNow(), result.Snapshot.CreatedAt);
Assert.Equal(PolicyScoringConfig.Default.Version, result.Snapshot.ScoringConfig.Version);
var latest = await store.GetLatestAsync();
Assert.Equal(result.Snapshot, latest);
var audits = await auditRepo.ListAsync(10);
Assert.Single(audits);
Assert.Equal(result.Digest, audits[0].Digest);
Assert.Equal("snapshot.created", audits[0].Action);
Assert.Equal("rev-1", audits[0].RevisionId);
}
[Fact]
public async Task SaveAsync_DoesNotCreateNewRevisionWhenDigestUnchanged()
{
var snapshotRepo = new InMemoryPolicySnapshotRepository();
var auditRepo = new InMemoryPolicyAuditRepository();
var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 18, 10, 0, 0, TimeSpan.Zero));
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, timeProvider, NullLogger<PolicySnapshotStore>.Instance);
var content = new PolicySnapshotContent(BasePolicyYaml, PolicyDocumentFormat.Yaml, "cli", "test", null);
var first = await store.SaveAsync(content, CancellationToken.None);
Assert.True(first.Created);
timeProvider.Advance(TimeSpan.FromHours(1));
var second = await store.SaveAsync(content, CancellationToken.None);
Assert.True(second.Success);
Assert.False(second.Created);
Assert.Equal(first.Digest, second.Digest);
Assert.Equal("rev-1", second.Snapshot!.RevisionId);
Assert.Equal(PolicyScoringConfig.Default.Version, second.Snapshot.ScoringConfig.Version);
var audits = await auditRepo.ListAsync(10);
Assert.Single(audits);
}
[Fact]
public async Task SaveAsync_ReturnsFailureWhenValidationFails()
{
var snapshotRepo = new InMemoryPolicySnapshotRepository();
var auditRepo = new InMemoryPolicyAuditRepository();
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, TimeProvider.System, NullLogger<PolicySnapshotStore>.Instance);
const string invalidYaml = "version: '1.0'\nrules: []";
var content = new PolicySnapshotContent(invalidYaml, PolicyDocumentFormat.Yaml, null, null, null);
var result = await store.SaveAsync(content, CancellationToken.None);
Assert.False(result.Success);
Assert.False(result.Created);
Assert.Null(result.Snapshot);
var audits = await auditRepo.ListAsync(5);
Assert.Empty(audits);
}
}
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Time.Testing;
using Xunit;
namespace StellaOps.Policy.Tests;
public sealed class PolicySnapshotStoreTests
{
private const string BasePolicyYaml = """
version: "1.0"
rules:
- name: Block Critical
severity: [Critical]
action: block
""";
[Fact]
public async Task SaveAsync_CreatesNewSnapshotAndAuditEntry()
{
var snapshotRepo = new InMemoryPolicySnapshotRepository();
var auditRepo = new InMemoryPolicyAuditRepository();
var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 18, 10, 0, 0, TimeSpan.Zero));
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, timeProvider, NullLogger<PolicySnapshotStore>.Instance);
var content = new PolicySnapshotContent(BasePolicyYaml, PolicyDocumentFormat.Yaml, "cli", "test", null);
var result = await store.SaveAsync(content, CancellationToken.None);
Assert.True(result.Success);
Assert.True(result.Created);
Assert.NotNull(result.Snapshot);
Assert.Equal("rev-1", result.Snapshot!.RevisionId);
Assert.Equal(result.Digest, result.Snapshot.Digest);
Assert.Equal(timeProvider.GetUtcNow(), result.Snapshot.CreatedAt);
Assert.Equal(PolicyScoringConfig.Default.Version, result.Snapshot.ScoringConfig.Version);
var latest = await store.GetLatestAsync();
Assert.Equal(result.Snapshot, latest);
var audits = await auditRepo.ListAsync(10);
Assert.Single(audits);
Assert.Equal(result.Digest, audits[0].Digest);
Assert.Equal("snapshot.created", audits[0].Action);
Assert.Equal("rev-1", audits[0].RevisionId);
}
[Fact]
public async Task SaveAsync_DoesNotCreateNewRevisionWhenDigestUnchanged()
{
var snapshotRepo = new InMemoryPolicySnapshotRepository();
var auditRepo = new InMemoryPolicyAuditRepository();
var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 18, 10, 0, 0, TimeSpan.Zero));
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, timeProvider, NullLogger<PolicySnapshotStore>.Instance);
var content = new PolicySnapshotContent(BasePolicyYaml, PolicyDocumentFormat.Yaml, "cli", "test", null);
var first = await store.SaveAsync(content, CancellationToken.None);
Assert.True(first.Created);
timeProvider.Advance(TimeSpan.FromHours(1));
var second = await store.SaveAsync(content, CancellationToken.None);
Assert.True(second.Success);
Assert.False(second.Created);
Assert.Equal(first.Digest, second.Digest);
Assert.Equal("rev-1", second.Snapshot!.RevisionId);
Assert.Equal(PolicyScoringConfig.Default.Version, second.Snapshot.ScoringConfig.Version);
var audits = await auditRepo.ListAsync(10);
Assert.Single(audits);
}
[Fact]
public async Task SaveAsync_ReturnsFailureWhenValidationFails()
{
var snapshotRepo = new InMemoryPolicySnapshotRepository();
var auditRepo = new InMemoryPolicyAuditRepository();
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, TimeProvider.System, NullLogger<PolicySnapshotStore>.Instance);
const string invalidYaml = "version: '1.0'\nrules: []";
var content = new PolicySnapshotContent(invalidYaml, PolicyDocumentFormat.Yaml, null, null, null);
var result = await store.SaveAsync(content, CancellationToken.None);
Assert.False(result.Success);
Assert.False(result.Created);
Assert.Null(result.Snapshot);
var audits = await auditRepo.ListAsync(5);
Assert.Empty(audits);
}
}