219 lines
7.7 KiB
C#
219 lines
7.7 KiB
C#
using Xunit;
|
|
using Microsoft.Extensions.Time.Testing;
|
|
using StellaOps.Policy.Engine.Domain;
|
|
using StellaOps.Policy.Engine.Ledger;
|
|
using StellaOps.Policy.Engine.Orchestration;
|
|
using StellaOps.Policy.Engine.Services;
|
|
using StellaOps.Policy.Engine.Snapshots;
|
|
using StellaOps.Policy.Engine.TrustWeighting;
|
|
using StellaOps.Policy.Engine.Violations;
|
|
|
|
using StellaOps.TestKit;
|
|
namespace StellaOps.Policy.Engine.Tests;
|
|
|
|
public sealed class PolicyDecisionServiceTests
|
|
{
|
|
private static (PolicyDecisionService service, string snapshotId) BuildService()
|
|
{
|
|
var clock = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-27T10:00:00Z"));
|
|
|
|
var jobStore = new InMemoryOrchestratorJobStore();
|
|
var resultStore = new InMemoryWorkerResultStore(jobStore);
|
|
var exportStore = new InMemoryLedgerExportStore();
|
|
var ledger = new LedgerExportService(clock, jobStore, resultStore, exportStore);
|
|
var snapshotStore = new InMemorySnapshotStore();
|
|
var violationStore = new InMemoryViolationEventStore();
|
|
var trust = new TrustWeightingService(clock);
|
|
|
|
var snapshotService = new SnapshotService(clock, ledger, snapshotStore);
|
|
var eventService = new ViolationEventService(snapshotStore, jobStore, violationStore);
|
|
var fusionService = new SeverityFusionService(violationStore, trust);
|
|
var conflictService = new ConflictHandlingService(violationStore);
|
|
var evidenceService = new EvidenceSummaryService(clock);
|
|
|
|
var decisionService = new PolicyDecisionService(
|
|
eventService,
|
|
fusionService,
|
|
conflictService,
|
|
evidenceService);
|
|
|
|
// Setup test data
|
|
var job = new OrchestratorJob(
|
|
JobId: "job-decision-test",
|
|
TenantId: "acme",
|
|
ContextId: "ctx",
|
|
PolicyProfileHash: "hash",
|
|
RequestedAt: clock.GetUtcNow(),
|
|
Priority: "normal",
|
|
BatchItems: new[]
|
|
{
|
|
new OrchestratorJobItem("pkg:npm/lodash@4.17.21", "CVE-2021-23337"),
|
|
new OrchestratorJobItem("pkg:npm/axios@0.21.1", "CVE-2021-3749"),
|
|
new OrchestratorJobItem("pkg:maven/log4j@2.14.1", "CVE-2021-44228")
|
|
},
|
|
Callbacks: null,
|
|
TraceRef: "trace-decision",
|
|
Status: "completed",
|
|
DeterminismHash: "hash",
|
|
CompletedAt: clock.GetUtcNow(),
|
|
ResultHash: "res");
|
|
|
|
jobStore.SaveAsync(job).GetAwaiter().GetResult();
|
|
|
|
resultStore.SaveAsync(new WorkerRunResult(
|
|
job.JobId,
|
|
"worker-decision",
|
|
clock.GetUtcNow(),
|
|
clock.GetUtcNow(),
|
|
new[]
|
|
{
|
|
new WorkerResultItem("pkg:npm/lodash@4.17.21", "CVE-2021-23337", "violation", "trace-lodash"),
|
|
new WorkerResultItem("pkg:npm/axios@0.21.1", "CVE-2021-3749", "warn", "trace-axios"),
|
|
new WorkerResultItem("pkg:maven/log4j@2.14.1", "CVE-2021-44228", "violation", "trace-log4j")
|
|
},
|
|
"hash")).GetAwaiter().GetResult();
|
|
|
|
ledger.BuildAsync(new LedgerExportRequest("acme")).GetAwaiter().GetResult();
|
|
var snapshot = snapshotService.CreateAsync(new SnapshotRequest("acme", "overlay-decision")).GetAwaiter().GetResult();
|
|
|
|
return (decisionService, snapshot.SnapshotId);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task GetDecisionsAsync_ReturnsDecisionsWithEvidence()
|
|
{
|
|
var (service, snapshotId) = BuildService();
|
|
var request = new PolicyDecisionRequest(snapshotId);
|
|
|
|
var response = await service.GetDecisionsAsync(request);
|
|
|
|
Assert.Equal(snapshotId, response.SnapshotId);
|
|
Assert.Equal(3, response.Decisions.Count);
|
|
Assert.All(response.Decisions, d =>
|
|
{
|
|
Assert.False(string.IsNullOrWhiteSpace(d.SeverityFused));
|
|
Assert.NotNull(d.Evidence);
|
|
Assert.NotNull(d.TopSources);
|
|
Assert.True(d.TopSources.Count > 0);
|
|
});
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task GetDecisionsAsync_BuildsSummaryStatistics()
|
|
{
|
|
var (service, snapshotId) = BuildService();
|
|
var request = new PolicyDecisionRequest(snapshotId);
|
|
|
|
var response = await service.GetDecisionsAsync(request);
|
|
|
|
Assert.Equal(3, response.Summary.TotalDecisions);
|
|
Assert.NotEmpty(response.Summary.SeverityCounts);
|
|
Assert.NotEmpty(response.Summary.TopSeveritySources);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task GetDecisionsAsync_FiltersById()
|
|
{
|
|
var (service, snapshotId) = BuildService();
|
|
var request = new PolicyDecisionRequest(
|
|
SnapshotId: snapshotId,
|
|
AdvisoryId: "CVE-2021-44228");
|
|
|
|
var response = await service.GetDecisionsAsync(request);
|
|
|
|
Assert.Single(response.Decisions);
|
|
Assert.Equal("CVE-2021-44228", response.Decisions[0].AdvisoryId);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task GetDecisionsAsync_FiltersByTenant()
|
|
{
|
|
var (service, snapshotId) = BuildService();
|
|
var request = new PolicyDecisionRequest(
|
|
SnapshotId: snapshotId,
|
|
TenantId: "acme");
|
|
|
|
var response = await service.GetDecisionsAsync(request);
|
|
|
|
Assert.All(response.Decisions, d => Assert.Equal("acme", d.TenantId));
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task GetDecisionsAsync_LimitsTopSources()
|
|
{
|
|
var (service, snapshotId) = BuildService();
|
|
var request = new PolicyDecisionRequest(
|
|
SnapshotId: snapshotId,
|
|
MaxSources: 1);
|
|
|
|
var response = await service.GetDecisionsAsync(request);
|
|
|
|
Assert.All(response.Decisions, d =>
|
|
{
|
|
Assert.True(d.TopSources.Count <= 1);
|
|
});
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task GetDecisionsAsync_ExcludesEvidenceWhenNotRequested()
|
|
{
|
|
var (service, snapshotId) = BuildService();
|
|
var request = new PolicyDecisionRequest(
|
|
SnapshotId: snapshotId,
|
|
IncludeEvidence: false);
|
|
|
|
var response = await service.GetDecisionsAsync(request);
|
|
|
|
Assert.All(response.Decisions, d => Assert.Null(d.Evidence));
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task GetDecisionsAsync_ReturnsDeterministicOrder()
|
|
{
|
|
var (service, snapshotId) = BuildService();
|
|
var request = new PolicyDecisionRequest(snapshotId);
|
|
|
|
var response1 = await service.GetDecisionsAsync(request);
|
|
var response2 = await service.GetDecisionsAsync(request);
|
|
|
|
Assert.Equal(
|
|
response1.Decisions.Select(d => d.ComponentPurl),
|
|
response2.Decisions.Select(d => d.ComponentPurl));
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task GetDecisionsAsync_ThrowsOnEmptySnapshotId()
|
|
{
|
|
var (service, _) = BuildService();
|
|
var request = new PolicyDecisionRequest(string.Empty);
|
|
|
|
await Assert.ThrowsAsync<ArgumentException>(() => service.GetDecisionsAsync(request));
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task GetDecisionsAsync_TopSourcesHaveRanks()
|
|
{
|
|
var (service, snapshotId) = BuildService();
|
|
var request = new PolicyDecisionRequest(snapshotId);
|
|
|
|
var response = await service.GetDecisionsAsync(request);
|
|
|
|
foreach (var decision in response.Decisions)
|
|
{
|
|
for (var i = 0; i < decision.TopSources.Count; i++)
|
|
{
|
|
Assert.Equal(i + 1, decision.TopSources[i].Rank);
|
|
}
|
|
}
|
|
}
|
|
}
|