Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Temp commit to debug
187 lines
7.1 KiB
C#
187 lines
7.1 KiB
C#
using System.Net;
|
|
using System.Net.Http;
|
|
using System.Text.Json.Nodes;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Microsoft.Extensions.Options;
|
|
using StellaOps.Findings.Ledger.Domain;
|
|
using StellaOps.Findings.Ledger.Infrastructure.Policy;
|
|
using StellaOps.Findings.Ledger.Options;
|
|
using Xunit;
|
|
|
|
namespace StellaOps.Findings.Ledger.Tests;
|
|
|
|
public sealed class PolicyEngineEvaluationServiceTests
|
|
{
|
|
private const string TenantId = "tenant-1";
|
|
private static readonly DateTimeOffset Now = DateTimeOffset.UtcNow;
|
|
|
|
[Fact]
|
|
public async Task EvaluateAsync_UsesPolicyEngineAndCachesResult()
|
|
{
|
|
var handler = new StubHttpHandler(_ => new HttpResponseMessage(HttpStatusCode.OK)
|
|
{
|
|
Content = new StringContent(
|
|
"""
|
|
{
|
|
"items": [
|
|
{
|
|
"findingId": "finding-1",
|
|
"status": "affected",
|
|
"severity": 7.5,
|
|
"labels": { "exposure": "runtime" },
|
|
"explainRef": "policy://explain/123",
|
|
"rationale": ["policy://explain/123"]
|
|
}
|
|
]
|
|
}
|
|
""",
|
|
System.Text.Encoding.UTF8,
|
|
"application/json")
|
|
});
|
|
|
|
var factory = new TestHttpClientFactory(handler);
|
|
var options = CreateOptions(new Uri("https://policy.example/"));
|
|
using var cache = new PolicyEvaluationCache(options.PolicyEngine, NullLogger<PolicyEvaluationCache>.Instance);
|
|
var inline = new InlinePolicyEvaluationService(NullLogger<InlinePolicyEvaluationService>.Instance);
|
|
var service = new PolicyEngineEvaluationService(factory, inline, cache, Microsoft.Extensions.Options.Options.Create(options), NullLogger<PolicyEngineEvaluationService>.Instance);
|
|
|
|
var record = CreateRecord();
|
|
|
|
var first = await service.EvaluateAsync(record, existingProjection: null, CancellationToken.None);
|
|
var second = await service.EvaluateAsync(record, existingProjection: null, CancellationToken.None);
|
|
|
|
Assert.Equal("affected", first.Status);
|
|
Assert.Equal(7.5m, first.Severity);
|
|
Assert.Equal("policy://explain/123", first.ExplainRef);
|
|
Assert.Equal("runtime", first.Labels?["exposure"]?.GetValue<string>());
|
|
Assert.Equal(1, handler.CallCount); // cached second call
|
|
Assert.Equal("affected", second.Status);
|
|
Assert.Equal("policy://explain/123", second.Rationale[0]?.GetValue<string>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EvaluateAsync_FallsBackToInlineWhenRequestFails()
|
|
{
|
|
var handler = new StubHttpHandler(_ => new HttpResponseMessage(HttpStatusCode.InternalServerError));
|
|
var factory = new TestHttpClientFactory(handler);
|
|
var options = CreateOptions(new Uri("https://policy.example/"));
|
|
using var cache = new PolicyEvaluationCache(options.PolicyEngine, NullLogger<PolicyEvaluationCache>.Instance);
|
|
var inline = new InlinePolicyEvaluationService(NullLogger<InlinePolicyEvaluationService>.Instance);
|
|
var service = new PolicyEngineEvaluationService(factory, inline, cache, Microsoft.Extensions.Options.Options.Create(options), NullLogger<PolicyEngineEvaluationService>.Instance);
|
|
|
|
var record = CreateRecord(payloadStatus: "investigating", payloadSeverity: 4.2m);
|
|
|
|
var result = await service.EvaluateAsync(record, existingProjection: null, CancellationToken.None);
|
|
|
|
Assert.Equal("investigating", result.Status);
|
|
Assert.Equal(4.2m, result.Severity);
|
|
Assert.Equal(1, handler.CallCount);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EvaluateAsync_UsesInlineWhenNoBaseAddressConfigured()
|
|
{
|
|
var handler = new StubHttpHandler(_ => throw new InvalidOperationException("Handler should not be invoked."));
|
|
var factory = new TestHttpClientFactory(handler);
|
|
var options = CreateOptions(baseAddress: null);
|
|
using var cache = new PolicyEvaluationCache(options.PolicyEngine, NullLogger<PolicyEvaluationCache>.Instance);
|
|
var inline = new InlinePolicyEvaluationService(NullLogger<InlinePolicyEvaluationService>.Instance);
|
|
var service = new PolicyEngineEvaluationService(factory, inline, cache, Microsoft.Extensions.Options.Options.Create(options), NullLogger<PolicyEngineEvaluationService>.Instance);
|
|
|
|
var record = CreateRecord(payloadStatus: "accepted_risk", payloadSeverity: 1.0m);
|
|
|
|
var result = await service.EvaluateAsync(record, existingProjection: null, CancellationToken.None);
|
|
|
|
Assert.Equal("accepted_risk", result.Status);
|
|
Assert.Equal(1.0m, result.Severity);
|
|
Assert.Equal(0, handler.CallCount);
|
|
}
|
|
|
|
private static LedgerServiceOptions CreateOptions(Uri? baseAddress)
|
|
{
|
|
return new LedgerServiceOptions
|
|
{
|
|
PolicyEngine = new LedgerServiceOptions.PolicyEngineOptions
|
|
{
|
|
BaseAddress = baseAddress,
|
|
Cache = new LedgerServiceOptions.PolicyEngineCacheOptions
|
|
{
|
|
SizeLimit = 16,
|
|
EntryLifetime = TimeSpan.FromMinutes(5)
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
private static LedgerEventRecord CreateRecord(string payloadStatus = "affected", decimal? payloadSeverity = 5.0m)
|
|
{
|
|
var payload = new JsonObject
|
|
{
|
|
["status"] = payloadStatus,
|
|
["severity"] = payloadSeverity,
|
|
["labels"] = new JsonObject { ["source"] = "policy" }
|
|
};
|
|
|
|
var envelope = new JsonObject
|
|
{
|
|
["event"] = new JsonObject
|
|
{
|
|
["payload"] = payload
|
|
}
|
|
};
|
|
|
|
return new LedgerEventRecord(
|
|
TenantId,
|
|
Guid.NewGuid(),
|
|
1,
|
|
Guid.NewGuid(),
|
|
LedgerEventConstants.EventFindingStatusChanged,
|
|
"policy/v1",
|
|
"finding-1",
|
|
"artifact-1",
|
|
null,
|
|
"actor",
|
|
"service",
|
|
Now,
|
|
Now,
|
|
envelope,
|
|
"hash",
|
|
"prev",
|
|
"leaf",
|
|
envelope.ToJsonString());
|
|
}
|
|
|
|
private sealed class StubHttpHandler : HttpMessageHandler
|
|
{
|
|
private readonly Func<HttpRequestMessage, HttpResponseMessage> _handler;
|
|
|
|
public StubHttpHandler(Func<HttpRequestMessage, HttpResponseMessage> handler)
|
|
{
|
|
_handler = handler ?? throw new ArgumentNullException(nameof(handler));
|
|
}
|
|
|
|
public int CallCount { get; private set; }
|
|
|
|
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
|
{
|
|
CallCount++;
|
|
return Task.FromResult(_handler(request));
|
|
}
|
|
}
|
|
|
|
private sealed class TestHttpClientFactory : IHttpClientFactory
|
|
{
|
|
private readonly HttpMessageHandler _handler;
|
|
|
|
public TestHttpClientFactory(HttpMessageHandler handler)
|
|
{
|
|
_handler = handler;
|
|
}
|
|
|
|
public HttpClient CreateClient(string name)
|
|
{
|
|
return new HttpClient(_handler, disposeHandler: false);
|
|
}
|
|
}
|
|
}
|