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;
}
}
}

View File

@@ -0,0 +1,20 @@
using FluentAssertions;
using StellaOps.Policy.Scoring.Engine;
using Xunit;
namespace StellaOps.Policy.Scoring.Tests;
public class CvssVectorInteropTests
{
[Theory]
[InlineData("CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", "CVSS:4.0/AV:N/AC:L/PR:N/UI:N/VC:H/VI:H/VA:H")]
[InlineData("CVSS:3.1/AV:L/AC:H/PR:H/UI:R/S:U/C:L/I:L/A:L", "CVSS:4.0/AV:L/AC:H/PR:H/UI:R/VC:L/VI:L/VA:L")]
public void ConvertV31ToV4_ProducesDeterministicVector(string v31, string expectedPrefix)
{
var v4 = CvssVectorInterop.ConvertV31ToV4(v31);
v4.Should().StartWith(expectedPrefix);
// determinism: same input produces identical output
CvssVectorInterop.ConvertV31ToV4(v31).Should().Be(v4);
}
}

View File

@@ -1,5 +1,6 @@
using System.Collections.Immutable;
using FluentAssertions;
using System.Linq;
using StellaOps.Attestor.Envelope;
using StellaOps.Policy.Scoring.Engine;
using StellaOps.Policy.Scoring.Receipts;
@@ -71,6 +72,74 @@ public sealed class ReceiptBuilderTests
_repository.Contains(receipt1.ReceiptId).Should().BeTrue();
}
[Fact]
public async Task CreateAsync_InputHashIgnoresPropertyOrder()
{
var policy = new CvssPolicy
{
PolicyId = "default",
Version = "1.0.0",
Name = "Default",
EffectiveFrom = new DateTimeOffset(2025, 01, 01, 0, 0, 0, TimeSpan.Zero),
Hash = "abc123",
};
var baseMetrics = new CvssBaseMetrics
{
AttackVector = AttackVector.Network,
AttackComplexity = AttackComplexity.Low,
AttackRequirements = AttackRequirements.None,
PrivilegesRequired = PrivilegesRequired.None,
UserInteraction = UserInteraction.None,
VulnerableSystemConfidentiality = ImpactMetricValue.High,
VulnerableSystemIntegrity = ImpactMetricValue.High,
VulnerableSystemAvailability = ImpactMetricValue.High,
SubsequentSystemConfidentiality = ImpactMetricValue.High,
SubsequentSystemIntegrity = ImpactMetricValue.High,
SubsequentSystemAvailability = ImpactMetricValue.High
};
var evidence1 = ImmutableList<CvssEvidenceItem>.Empty.Add(new CvssEvidenceItem
{
Type = "scan",
Uri = "sha256:1",
Description = "First",
IsAuthoritative = false
}).Add(new CvssEvidenceItem
{
Type = "advisory",
Uri = "sha256:2",
Description = "Second",
IsAuthoritative = true
});
var evidence2 = evidence1.Reverse().ToImmutableList();
var builder = new ReceiptBuilder(_engine, _repository);
var r1 = await builder.CreateAsync(new CreateReceiptRequest
{
VulnerabilityId = "CVE-2025-1111",
TenantId = "t",
CreatedBy = "u",
Policy = policy,
BaseMetrics = baseMetrics,
Evidence = evidence1
});
var r2 = await builder.CreateAsync(new CreateReceiptRequest
{
VulnerabilityId = "CVE-2025-1111",
TenantId = "t",
CreatedBy = "u",
Policy = policy,
BaseMetrics = baseMetrics,
Evidence = evidence2
});
r1.InputHash.Should().Be(r2.InputHash);
}
[Fact]
public async Task CreateAsync_WithSigningKey_AttachesDsseReference()
{