up
Some checks failed
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
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Some checks failed
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
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,319 @@
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Policy.Engine.IncrementalOrchestrator;
|
||||
using StellaOps.Policy.Engine.Telemetry;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Tests.IncrementalOrchestrator;
|
||||
|
||||
public sealed class IncrementalOrchestratorTests
|
||||
{
|
||||
#region PolicyChangeEvent Tests
|
||||
|
||||
[Fact]
|
||||
public void CreateAdvisoryUpdated_CreatesValidEvent()
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var evt = PolicyChangeEventFactory.CreateAdvisoryUpdated(
|
||||
tenantId: "test-tenant",
|
||||
advisoryId: "GHSA-test-001",
|
||||
vulnerabilityId: "CVE-2021-12345",
|
||||
affectedPurls: ["pkg:npm/lodash", "pkg:npm/express"],
|
||||
source: "concelier",
|
||||
occurredAt: now,
|
||||
createdAt: now);
|
||||
|
||||
evt.ChangeType.Should().Be(PolicyChangeType.AdvisoryUpdated);
|
||||
evt.TenantId.Should().Be("test-tenant");
|
||||
evt.AdvisoryId.Should().Be("GHSA-test-001");
|
||||
evt.VulnerabilityId.Should().Be("CVE-2021-12345");
|
||||
evt.AffectedPurls.Should().HaveCount(2);
|
||||
evt.EventId.Should().StartWith("pce-");
|
||||
evt.ContentHash.Should().NotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateVexUpdated_CreatesValidEvent()
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var evt = PolicyChangeEventFactory.CreateVexUpdated(
|
||||
tenantId: "test-tenant",
|
||||
vulnerabilityId: "CVE-2021-12345",
|
||||
affectedProductKeys: ["pkg:npm/lodash"],
|
||||
source: "excititor",
|
||||
occurredAt: now,
|
||||
createdAt: now);
|
||||
|
||||
evt.ChangeType.Should().Be(PolicyChangeType.VexStatementUpdated);
|
||||
evt.VulnerabilityId.Should().Be("CVE-2021-12345");
|
||||
evt.AffectedProductKeys.Should().ContainSingle();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateSbomUpdated_CreatesValidEvent()
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var evt = PolicyChangeEventFactory.CreateSbomUpdated(
|
||||
tenantId: "test-tenant",
|
||||
sbomId: "sbom-123",
|
||||
productKey: "myapp:v1.0.0",
|
||||
componentPurls: ["pkg:npm/lodash@4.17.21"],
|
||||
source: "scanner",
|
||||
occurredAt: now,
|
||||
createdAt: now);
|
||||
|
||||
evt.ChangeType.Should().Be(PolicyChangeType.SbomUpdated);
|
||||
evt.AffectedSbomIds.Should().Contain("sbom-123");
|
||||
evt.AffectedProductKeys.Should().Contain("myapp:v1.0.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeContentHash_IsDeterministic()
|
||||
{
|
||||
var hash1 = PolicyChangeEvent.ComputeContentHash(
|
||||
PolicyChangeType.AdvisoryUpdated,
|
||||
"tenant",
|
||||
"ADV-001",
|
||||
"CVE-001",
|
||||
["pkg:npm/a", "pkg:npm/b"],
|
||||
null,
|
||||
null);
|
||||
|
||||
var hash2 = PolicyChangeEvent.ComputeContentHash(
|
||||
PolicyChangeType.AdvisoryUpdated,
|
||||
"tenant",
|
||||
"ADV-001",
|
||||
"CVE-001",
|
||||
["pkg:npm/b", "pkg:npm/a"], // Different order
|
||||
null,
|
||||
null);
|
||||
|
||||
hash1.Should().Be(hash2); // Should be equal due to sorting
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeContentHash_DiffersForDifferentInput()
|
||||
{
|
||||
var hash1 = PolicyChangeEvent.ComputeContentHash(
|
||||
PolicyChangeType.AdvisoryUpdated,
|
||||
"tenant",
|
||||
"ADV-001",
|
||||
"CVE-001",
|
||||
null, null, null);
|
||||
|
||||
var hash2 = PolicyChangeEvent.ComputeContentHash(
|
||||
PolicyChangeType.AdvisoryUpdated,
|
||||
"tenant",
|
||||
"ADV-002", // Different advisory
|
||||
"CVE-001",
|
||||
null, null, null);
|
||||
|
||||
hash1.Should().NotBe(hash2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateManualTrigger_IncludesRequestedBy()
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var evt = PolicyChangeEventFactory.CreateManualTrigger(
|
||||
tenantId: "test-tenant",
|
||||
policyIds: ["policy-1"],
|
||||
sbomIds: ["sbom-1"],
|
||||
productKeys: null,
|
||||
requestedBy: "admin@example.com",
|
||||
createdAt: now);
|
||||
|
||||
evt.ChangeType.Should().Be(PolicyChangeType.ManualTrigger);
|
||||
evt.Metadata.Should().ContainKey("requestedBy");
|
||||
evt.Metadata["requestedBy"].Should().Be("admin@example.com");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IncrementalPolicyOrchestrator Tests
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessAsync_ProcessesEvents()
|
||||
{
|
||||
var eventSource = new InMemoryPolicyChangeEventSource();
|
||||
var submitter = new TestSubmitter();
|
||||
var idempotencyStore = new InMemoryPolicyChangeIdempotencyStore();
|
||||
var timeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow);
|
||||
|
||||
var orchestrator = new IncrementalPolicyOrchestrator(
|
||||
eventSource, submitter, idempotencyStore,
|
||||
timeProvider: timeProvider);
|
||||
|
||||
eventSource.Enqueue(PolicyChangeEventFactory.CreateAdvisoryUpdated(
|
||||
"tenant1", "ADV-001", "CVE-001", ["pkg:npm/test"],
|
||||
"test", timeProvider.GetUtcNow(), timeProvider.GetUtcNow()));
|
||||
|
||||
var result = await orchestrator.ProcessAsync(CancellationToken.None);
|
||||
|
||||
result.TotalEventsRead.Should().Be(1);
|
||||
result.BatchesProcessed.Should().Be(1);
|
||||
submitter.SubmittedBatches.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessAsync_DeduplicatesEvents()
|
||||
{
|
||||
var eventSource = new InMemoryPolicyChangeEventSource();
|
||||
var submitter = new TestSubmitter();
|
||||
var idempotencyStore = new InMemoryPolicyChangeIdempotencyStore();
|
||||
var timeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow);
|
||||
|
||||
var orchestrator = new IncrementalPolicyOrchestrator(
|
||||
eventSource, submitter, idempotencyStore,
|
||||
timeProvider: timeProvider);
|
||||
|
||||
var evt = PolicyChangeEventFactory.CreateAdvisoryUpdated(
|
||||
"tenant1", "ADV-001", "CVE-001", ["pkg:npm/test"],
|
||||
"test", timeProvider.GetUtcNow(), timeProvider.GetUtcNow());
|
||||
|
||||
// Mark as already seen
|
||||
await idempotencyStore.MarkSeenAsync(evt.EventId, timeProvider.GetUtcNow(), CancellationToken.None);
|
||||
|
||||
eventSource.Enqueue(evt);
|
||||
|
||||
var result = await orchestrator.ProcessAsync(CancellationToken.None);
|
||||
|
||||
result.TotalEventsRead.Should().Be(1);
|
||||
result.EventsSkippedDuplicate.Should().Be(1);
|
||||
result.BatchesProcessed.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessAsync_SkipsOldEvents()
|
||||
{
|
||||
var options = new IncrementalOrchestratorOptions
|
||||
{
|
||||
MaxEventAge = TimeSpan.FromHours(1)
|
||||
};
|
||||
var eventSource = new InMemoryPolicyChangeEventSource();
|
||||
var submitter = new TestSubmitter();
|
||||
var idempotencyStore = new InMemoryPolicyChangeIdempotencyStore();
|
||||
var timeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow);
|
||||
|
||||
var orchestrator = new IncrementalPolicyOrchestrator(
|
||||
eventSource, submitter, idempotencyStore, options,
|
||||
timeProvider: timeProvider);
|
||||
|
||||
// Create an old event
|
||||
var oldTime = timeProvider.GetUtcNow().AddHours(-2);
|
||||
var evt = PolicyChangeEventFactory.CreateAdvisoryUpdated(
|
||||
"tenant1", "ADV-001", "CVE-001", ["pkg:npm/test"],
|
||||
"test", oldTime, oldTime);
|
||||
|
||||
eventSource.Enqueue(evt);
|
||||
|
||||
var result = await orchestrator.ProcessAsync(CancellationToken.None);
|
||||
|
||||
result.TotalEventsRead.Should().Be(1);
|
||||
result.EventsSkippedOld.Should().Be(1);
|
||||
result.BatchesProcessed.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessAsync_GroupsByTenant()
|
||||
{
|
||||
var eventSource = new InMemoryPolicyChangeEventSource();
|
||||
var submitter = new TestSubmitter();
|
||||
var idempotencyStore = new InMemoryPolicyChangeIdempotencyStore();
|
||||
var timeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow);
|
||||
|
||||
var orchestrator = new IncrementalPolicyOrchestrator(
|
||||
eventSource, submitter, idempotencyStore,
|
||||
timeProvider: timeProvider);
|
||||
|
||||
eventSource.Enqueue(PolicyChangeEventFactory.CreateAdvisoryUpdated(
|
||||
"tenant1", "ADV-001", "CVE-001", [], "test",
|
||||
timeProvider.GetUtcNow(), timeProvider.GetUtcNow()));
|
||||
|
||||
eventSource.Enqueue(PolicyChangeEventFactory.CreateAdvisoryUpdated(
|
||||
"tenant2", "ADV-002", "CVE-002", [], "test",
|
||||
timeProvider.GetUtcNow(), timeProvider.GetUtcNow()));
|
||||
|
||||
var result = await orchestrator.ProcessAsync(CancellationToken.None);
|
||||
|
||||
result.BatchesProcessed.Should().Be(2); // One per tenant
|
||||
submitter.SubmittedBatches.Select(b => b.TenantId).Should()
|
||||
.BeEquivalentTo(["tenant1", "tenant2"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessAsync_SortsByPriority()
|
||||
{
|
||||
var eventSource = new InMemoryPolicyChangeEventSource();
|
||||
var submitter = new TestSubmitter();
|
||||
var idempotencyStore = new InMemoryPolicyChangeIdempotencyStore();
|
||||
var timeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow);
|
||||
|
||||
var orchestrator = new IncrementalPolicyOrchestrator(
|
||||
eventSource, submitter, idempotencyStore,
|
||||
timeProvider: timeProvider);
|
||||
|
||||
// Add normal priority first
|
||||
eventSource.Enqueue(PolicyChangeEventFactory.CreateAdvisoryUpdated(
|
||||
"tenant1", "ADV-001", "CVE-001", [], "test",
|
||||
timeProvider.GetUtcNow(), timeProvider.GetUtcNow(),
|
||||
priority: PolicyChangePriority.Normal));
|
||||
|
||||
// Add emergency priority second
|
||||
eventSource.Enqueue(PolicyChangeEventFactory.CreateAdvisoryUpdated(
|
||||
"tenant1", "ADV-002", "CVE-002", [], "test",
|
||||
timeProvider.GetUtcNow(), timeProvider.GetUtcNow(),
|
||||
priority: PolicyChangePriority.Emergency));
|
||||
|
||||
await orchestrator.ProcessAsync(CancellationToken.None);
|
||||
|
||||
// Emergency should be processed first (separate batch due to priority)
|
||||
submitter.SubmittedBatches.Should().HaveCount(2);
|
||||
submitter.SubmittedBatches[0].Priority.Should().Be(PolicyChangePriority.Emergency);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region RuleHitSamplingOptions Tests
|
||||
|
||||
[Fact]
|
||||
public void Default_HasReasonableSamplingRates()
|
||||
{
|
||||
var options = RuleHitSamplingOptions.Default;
|
||||
|
||||
options.BaseSamplingRate.Should().BeInRange(0.0, 1.0);
|
||||
options.VexOverrideSamplingRate.Should().Be(1.0); // Always sample VEX
|
||||
options.IncidentModeSamplingRate.Should().Be(1.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FullSampling_SamplesEverything()
|
||||
{
|
||||
var options = RuleHitSamplingOptions.FullSampling;
|
||||
|
||||
options.BaseSamplingRate.Should().Be(1.0);
|
||||
options.VexOverrideSamplingRate.Should().Be(1.0);
|
||||
options.HighSeveritySamplingRate.Should().Be(1.0);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private sealed class TestSubmitter : IPolicyReEvaluationSubmitter
|
||||
{
|
||||
public List<PolicyChangeBatch> SubmittedBatches { get; } = [];
|
||||
|
||||
public Task<PolicyReEvaluationResult> SubmitAsync(
|
||||
PolicyChangeBatch batch,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
SubmittedBatches.Add(batch);
|
||||
return Task.FromResult(new PolicyReEvaluationResult
|
||||
{
|
||||
Succeeded = true,
|
||||
JobIds = [$"job-{batch.BatchId}"],
|
||||
ProcessingTimeMs = 1
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user