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

This commit is contained in:
StellaOps Bot
2025-12-01 21:16:22 +02:00
parent c11d87d252
commit 909d9b6220
208 changed files with 860954 additions and 832 deletions

View File

@@ -0,0 +1,91 @@
using System;
using System.Collections.Generic;
using System.Linq;
using StellaOps.Policy;
using StellaOps.Policy.Engine.BatchEvaluation;
using StellaOps.Policy.Engine.Services;
using Xunit;
namespace StellaOps.Policy.Engine.Tests.BatchEvaluation;
public sealed class BatchEvaluationMapperTests
{
[Fact]
public void Validate_Fails_WhenTimestampMissing()
{
var request = new BatchEvaluationRequestDto(
TenantId: "acme",
Items: new[]
{
new BatchEvaluationItemDto(
PackId: "pack-1",
Version: 1,
SubjectPurl: "pkg:npm/lodash@4.17.21",
AdvisoryId: "ADV-1",
Severity: new EvaluationSeverityDto("high", 7.5m),
Advisory: new AdvisoryDto(new Dictionary<string, string>(), "nvd"),
Vex: new VexEvidenceDto(Array.Empty<VexStatementDto>()),
Sbom: new SbomDto(Array.Empty<string>()),
Exceptions: new ExceptionsDto(),
Reachability: new ReachabilityDto("unknown"),
EvaluationTimestamp: null)
});
var ok = BatchEvaluationValidator.TryValidate(request, out var error);
Assert.False(ok);
Assert.Contains("evaluationTimestamp", error, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void Mapper_Produces_RuntimeRequest_WithSuppliedValues()
{
var item = new BatchEvaluationItemDto(
PackId: "pack-1",
Version: 2,
SubjectPurl: "pkg:npm/foo@1.0.0",
AdvisoryId: "ADV-1",
Severity: new EvaluationSeverityDto("high", 8.0m),
Advisory: new AdvisoryDto(new Dictionary<string, string>
{
["cve"] = "CVE-2025-0001"
}, "nvd"),
Vex: new VexEvidenceDto(new[]
{
new VexStatementDto("not_affected", "vendor_confirmed", "stmt-1", new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero))
}),
Sbom: new SbomDto(
Tags: new[] { "runtime", "server" },
Components: new[]
{
new ComponentDto("foo", "1.0.0", "npm", "pkg:npm/foo@1.0.0")
}),
Exceptions: new ExceptionsDto(
Effects: new Dictionary<string, PolicyExceptionEffect>(),
Instances: new[]
{
new ExceptionInstanceDto(
Id: "ex-1",
EffectId: "suppress",
Scope: new ExceptionScopeDto(
RuleNames: new[] { "rule-1" },
Severities: new[] { "high" }),
CreatedAt: new DateTimeOffset(2025, 1, 2, 0, 0, 0, TimeSpan.Zero))
}),
Reachability: new ReachabilityDto("reachable", 0.9m, 0.8m, HasRuntimeEvidence: true, Source: "scanner", Method: "static", EvidenceRef: "evidence-1"),
EvaluationTimestamp: new DateTimeOffset(2025, 1, 3, 0, 0, 0, TimeSpan.Zero),
BypassCache: false);
var runtimeRequests = BatchEvaluationMapper.ToRuntimeRequests("acme", new[] { item });
var runtime = Assert.Single(runtimeRequests);
Assert.Equal("acme", runtime.TenantId);
Assert.Equal("pack-1", runtime.PackId);
Assert.Equal("pkg:npm/foo@1.0.0", runtime.SubjectPurl);
Assert.Equal(new DateTimeOffset(2025, 1, 3, 0, 0, 0, TimeSpan.Zero), runtime.EvaluationTimestamp);
Assert.Equal("reachable", runtime.Reachability.State);
Assert.True(runtime.Reachability.HasRuntimeEvidence);
Assert.Equal("scanner", runtime.Reachability.Source);
Assert.Equal("high", runtime.Severity.Normalized);
}
}

View File

@@ -5,6 +5,7 @@ using StellaOps.Policy.Engine.Caching;
using StellaOps.Policy.Engine.Compilation;
using StellaOps.Policy.Engine.Domain;
using StellaOps.Policy.Engine.Evaluation;
using StellaOps.Policy.Engine.ReachabilityFacts;
using StellaOps.Policy.Engine.Options;
using StellaOps.Policy.Engine.Services;
using StellaOps.PolicyDsl;
@@ -180,6 +181,55 @@ public sealed class PolicyRuntimeEvaluationServiceTests
Assert.NotEqual(response1.CorrelationId, response2.CorrelationId);
}
[Fact]
public async Task EvaluateAsync_EnrichesReachabilityFromFacts()
{
const string policy = """
policy "Reachability policy" syntax "stella-dsl@1" {
rule reachable_then_warn priority 5 {
when reachability.state == "reachable"
then status := "warn"
because "reachable path detected"
}
rule default priority 100 {
when true
then status := "affected"
because "default"
}
}
""";
var harness = CreateHarness();
await harness.StoreTestPolicyAsync("pack-2", 1, policy);
var fact = new ReachabilityFact
{
Id = "fact-1",
TenantId = "tenant-1",
ComponentPurl = "pkg:npm/lodash@4.17.21",
AdvisoryId = "CVE-2024-0001",
State = ReachabilityState.Reachable,
Confidence = 0.92m,
Score = 0.85m,
HasRuntimeEvidence = true,
Source = "graph-analyzer",
Method = AnalysisMethod.Hybrid,
EvidenceRef = "evidence/callgraph.json",
ComputedAt = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero),
ExpiresAt = null,
Metadata = new Dictionary<string, object?>()
};
await harness.ReachabilityStore.SaveAsync(fact, CancellationToken.None);
var request = CreateRequest("pack-2", 1, severity: "Low");
var response = await harness.Service.EvaluateAsync(request, CancellationToken.None);
Assert.Equal("warn", response.Status);
}
private static RuntimeEvaluationRequest CreateRequest(
string packId,
int version,
@@ -213,16 +263,28 @@ public sealed class PolicyRuntimeEvaluationServiceTests
var cache = new InMemoryPolicyEvaluationCache(cacheLogger, TimeProvider.System, options);
var evaluator = new PolicyEvaluator();
var reachabilityStore = new InMemoryReachabilityFactsStore(TimeProvider.System);
var reachabilityCache = new InMemoryReachabilityFactsOverlayCache(
NullLogger<InMemoryReachabilityFactsOverlayCache>.Instance,
TimeProvider.System,
Microsoft.Extensions.Options.Options.Create(new PolicyEngineOptions()));
var reachabilityService = new ReachabilityFactsJoiningService(
reachabilityStore,
reachabilityCache,
NullLogger<ReachabilityFactsJoiningService>.Instance,
TimeProvider.System);
var compilationService = CreateCompilationService();
var service = new PolicyRuntimeEvaluationService(
repository,
cache,
evaluator,
reachabilityService,
TimeProvider.System,
serviceLogger);
return new TestHarness(service, repository, compilationService);
return new TestHarness(service, repository, compilationService, reachabilityStore);
}
private static PolicyCompilationService CreateCompilationService()
@@ -238,7 +300,8 @@ public sealed class PolicyRuntimeEvaluationServiceTests
private sealed record TestHarness(
PolicyRuntimeEvaluationService Service,
InMemoryPolicyPackRepository Repository,
PolicyCompilationService CompilationService)
PolicyCompilationService CompilationService,
InMemoryReachabilityFactsStore ReachabilityStore)
{
public async Task StoreTestPolicyAsync(string packId, int version, string dsl)
{

View File

@@ -0,0 +1,88 @@
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Time.Testing;
using StellaOps.Policy.Engine.Events;
using StellaOps.Policy.Engine.Options;
using StellaOps.Policy.Engine.Storage.InMemory;
using StellaOps.Policy.Engine.Storage.Mongo.Documents;
using StellaOps.Policy.Engine.Workers;
using StellaOps.Policy.Engine.ExceptionCache;
using Xunit;
namespace StellaOps.Policy.Engine.Tests.Workers;
public sealed class ExceptionLifecycleServiceTests
{
[Fact]
public async Task Activates_pending_exceptions_and_publishes_event()
{
var time = new FakeTimeProvider(new DateTimeOffset(2025, 12, 1, 12, 0, 0, TimeSpan.Zero));
var repo = new InMemoryExceptionRepository();
await repo.CreateExceptionAsync(new PolicyExceptionDocument
{
Id = "exc-1",
TenantId = "tenant-a",
Status = "approved",
Name = "Test exception",
EffectiveFrom = time.GetUtcNow().AddMinutes(-1),
}, CancellationToken.None);
var publisher = new RecordingPublisher();
var options = Microsoft.Extensions.Options.Options.Create(new PolicyEngineOptions());
var service = new ExceptionLifecycleService(
repo,
publisher,
options,
time,
NullLogger<ExceptionLifecycleService>.Instance);
await service.ProcessOnceAsync(CancellationToken.None);
var updated = await repo.GetExceptionAsync("tenant-a", "exc-1", CancellationToken.None);
updated!.Status.Should().Be("active");
publisher.Events.Should().ContainSingle(e => e.EventType == "activated" && e.ExceptionId == "exc-1");
}
[Fact]
public async Task Expires_active_exceptions_and_publishes_event()
{
var time = new FakeTimeProvider(new DateTimeOffset(2025, 12, 1, 12, 0, 0, TimeSpan.Zero));
var repo = new InMemoryExceptionRepository();
await repo.CreateExceptionAsync(new PolicyExceptionDocument
{
Id = "exc-2",
TenantId = "tenant-b",
Status = "active",
Name = "Expiring exception",
ExpiresAt = time.GetUtcNow().AddMinutes(-1),
}, CancellationToken.None);
var publisher = new RecordingPublisher();
var options = Microsoft.Extensions.Options.Options.Create(new PolicyEngineOptions());
var service = new ExceptionLifecycleService(
repo,
publisher,
options,
time,
NullLogger<ExceptionLifecycleService>.Instance);
await service.ProcessOnceAsync(CancellationToken.None);
var updated = await repo.GetExceptionAsync("tenant-b", "exc-2", CancellationToken.None);
updated!.Status.Should().Be("expired");
publisher.Events.Should().ContainSingle(e => e.EventType == "expired" && e.ExceptionId == "exc-2");
}
private sealed class RecordingPublisher : IExceptionEventPublisher
{
public List<ExceptionEvent> Events { get; } = new();
public Task PublishAsync(ExceptionEvent exceptionEvent, CancellationToken cancellationToken = default)
{
Events.Add(exceptionEvent);
return Task.CompletedTask;
}
}
}